From 40760aa884e040434bdd36a1041e4daea9daba9d Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Fri, 12 Jun 2026 00:38:37 +0800 Subject: [PATCH] =?UTF-8?q?dudu=20MVP=EF=BC=9A=E4=BA=94=E7=AB=AF=E8=AF=AD?= =?UTF-8?q?=E9=9F=B3=E8=BE=93=E5=85=A5=E6=B3=95=E5=88=9D=E5=A7=8B=E6=8F=90?= =?UTF-8?q?=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - server:Go 网关(WS 流式识别中继/计费配额/微信登录支付 mock/反馈/埋点),gummy provider 已真实联调 - desktop:Tauri 2(全局快捷键 push-to-talk/浮层/托盘/设置/登录购买/反馈/首启引导) - android:Compose 主 App + IME(键盘内录音直传) - ios:App + 键盘扩展(1A spike 实证键盘内不可录音,走 deep link 听写) - design/design-pipeline:设计系统 + token 导出 iOS/Android 主题 - doc:前后端设计文档(HTML);web:官网宣传页;todo:任务看板 Co-Authored-By: Claude Fable 5 --- .github/workflows/ci.yml | 30 + .gitignore | 31 + README.md | 49 + android/.gitignore | 7 + android/app/build.gradle.kts | 64 + android/app/proguard-rules.pro | 1 + android/app/src/main/AndroidManifest.xml | 40 + .../src/main/java/app/dudu/app/BuyScreen.kt | 288 + .../app/src/main/java/app/dudu/app/DuduApp.kt | 15 + .../main/java/app/dudu/app/FeedbackScreen.kt | 306 + .../src/main/java/app/dudu/app/LoginScreen.kt | 154 + .../main/java/app/dudu/app/MainActivity.kt | 112 + .../src/main/java/app/dudu/app/MineScreen.kt | 248 + .../java/app/dudu/app/OnboardingScreen.kt | 183 + .../app/src/main/java/app/dudu/app/Setup.kt | 31 + android/app/src/main/java/app/dudu/app/Ui.kt | 299 + .../main/java/app/dudu/audio/AudioStreamer.kt | 76 + .../java/app/dudu/data/AccountRepository.kt | 207 + .../java/app/dudu/data/MetricsFlushWorker.kt | 61 + .../main/java/app/dudu/data/MetricsQueue.kt | 133 + .../src/main/java/app/dudu/design/DText.kt | 41 + .../main/java/app/dudu/design/DuduIcons.kt | 255 + .../main/java/app/dudu/design/DuduPalette.kt | 106 + .../main/java/app/dudu/design/DuduTheme.kt | 156 + .../main/java/app/dudu/ime/DuduImeService.kt | 143 + .../java/app/dudu/ime/KeyboardController.kt | 404 ++ .../java/app/dudu/ime/ui/KeyboardScreen.kt | 269 + .../src/main/java/app/dudu/ime/ui/MicBar.kt | 122 + .../src/main/java/app/dudu/ime/ui/Waveform.kt | 78 + .../src/main/java/app/dudu/net/AsrSocket.kt | 106 + .../app/src/main/java/app/dudu/net/DuduApi.kt | 262 + .../src/main/java/app/dudu/pay/PayClient.kt | 44 + .../main/java/app/dudu/wechat/WechatAuth.kt | 31 + .../res/drawable/ic_launcher_foreground.xml | 32 + .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../app/src/main/res/values-night/colors.xml | 4 + .../app/src/main/res/values-night/themes.xml | 9 + android/app/src/main/res/values/colors.xml | 6 + android/app/src/main/res/values/strings.xml | 5 + android/app/src/main/res/values/themes.xml | 10 + android/app/src/main/res/xml/method.xml | 9 + android/build.gradle.kts | 6 + android/gradle.properties | 4 + android/gradle/libs.versions.toml | 29 + android/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 53636 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 + android/gradlew | 160 + android/gradlew.bat | 90 + android/settings.gradle.kts | 18 + design-pipeline/export-tokens.mjs | 193 + .../generated/android/DuduTheme.kt | 156 + design-pipeline/generated/ios/DuduTheme.swift | 211 + design/SKILL.md | 15 + design/_adherence.oxlintrc.json | 387 ++ design/_ds_bundle.js | 2535 +++++++ design/_ds_manifest.json | 1 + design/assets/app-icon-dark.svg | 9 + design/assets/app-icon.svg | 9 + design/assets/logo-mark-blue.svg | 6 + design/assets/logo-mark-ink.svg | 6 + design/assets/logo-mark-white.svg | 6 + design/assets/logo-mark.svg | 6 + design/assets/wordmark.svg | 12 + design/components/core/Badge.d.ts | 7 + design/components/core/Badge.jsx | 23 + design/components/core/Badge.prompt.md | 7 + design/components/core/Button.d.ts | 16 + design/components/core/Button.jsx | 40 + design/components/core/Button.prompt.md | 10 + design/components/core/Card.d.ts | 8 + design/components/core/Card.jsx | 17 + design/components/core/Card.prompt.md | 8 + design/components/core/IconButton.d.ts | 12 + design/components/core/IconButton.jsx | 28 + design/components/core/IconButton.prompt.md | 10 + design/components/core/Kbd.d.ts | 7 + design/components/core/Kbd.jsx | 28 + design/components/core/Kbd.prompt.md | 9 + design/components/core/ProgressBar.d.ts | 11 + design/components/core/ProgressBar.jsx | 24 + design/components/core/ProgressBar.prompt.md | 8 + design/components/core/core.card.html | 67 + design/components/forms/Input.d.ts | 11 + design/components/forms/Input.jsx | 26 + design/components/forms/Input.prompt.md | 7 + design/components/forms/SettingRow.d.ts | 9 + design/components/forms/SettingRow.jsx | 30 + design/components/forms/SettingRow.prompt.md | 9 + design/components/forms/Switch.d.ts | 7 + design/components/forms/Switch.jsx | 33 + design/components/forms/Switch.prompt.md | 6 + design/components/forms/forms.card.html | 42 + design/components/voice/MicBar.d.ts | 13 + design/components/voice/MicBar.jsx | 54 + design/components/voice/MicBar.prompt.md | 11 + .../components/voice/RecognitionOverlay.d.ts | 19 + .../components/voice/RecognitionOverlay.jsx | 58 + .../voice/RecognitionOverlay.prompt.md | 11 + design/components/voice/Waveform.d.ts | 13 + design/components/voice/Waveform.jsx | 35 + design/components/voice/Waveform.prompt.md | 8 + design/components/voice/voice.card.html | 42 + design/guidelines/brand-logo.card.html | 23 + design/guidelines/brand-wordmark.card.html | 24 + design/guidelines/colors-brand.card.html | 31 + design/guidelines/colors-neutrals.card.html | 33 + design/guidelines/colors-overlay.card.html | 30 + design/guidelines/colors-semantic.card.html | 28 + design/guidelines/colors-surfaces.card.html | 44 + design/guidelines/motion.card.html | 24 + design/guidelines/spacing-controls.card.html | 20 + design/guidelines/spacing-elevation.card.html | 30 + design/guidelines/spacing-grid.card.html | 26 + design/guidelines/spacing-radius.card.html | 23 + design/guidelines/type-families.card.html | 31 + design/guidelines/type-recognition.card.html | 32 + design/guidelines/type-scale.card.html | 24 + design/readme.md | 101 + design/styles.css | 5 + design/tokens/colors.css | 113 + design/tokens/effects.css | 29 + design/tokens/fonts.css | 6 + design/tokens/spacing.css | 33 + design/tokens/typography.css | 38 + design/ui_kits/desktop/DesktopShared.jsx | 124 + design/ui_kits/desktop/LoginPurchase.jsx | 108 + design/ui_kits/desktop/OverlayDemo.jsx | 108 + design/ui_kits/desktop/README.md | 27 + design/ui_kits/desktop/SettingsTray.jsx | 140 + design/ui_kits/desktop/index.html | 110 + design/ui_kits/mobile/AppScreens.jsx | 187 + design/ui_kits/mobile/KeyboardScreen.jsx | 140 + design/ui_kits/mobile/MobileShared.jsx | 103 + design/ui_kits/mobile/README.md | 30 + design/ui_kits/mobile/index.html | 131 + desktop/feedback.html | 12 + desktop/index.html | 12 + desktop/login.html | 11 + desktop/onboarding.html | 12 + desktop/overlay.html | 12 + desktop/package-lock.json | 2036 ++++++ desktop/package.json | 22 + desktop/src-tauri/Cargo.lock | 6019 +++++++++++++++++ desktop/src-tauri/Cargo.toml | 30 + desktop/src-tauri/build.rs | 3 + desktop/src-tauri/capabilities/default.json | 7 + .../src-tauri/gen/schemas/acl-manifests.json | 1 + .../src-tauri/gen/schemas/capabilities.json | 1 + .../src-tauri/gen/schemas/desktop-schema.json | 2358 +++++++ .../src-tauri/gen/schemas/macOS-schema.json | 2358 +++++++ desktop/src-tauri/icons/icon.png | Bin 0 -> 2185 bytes desktop/src-tauri/src/api.rs | 292 + desktop/src-tauri/src/audio.rs | 185 + desktop/src-tauri/src/dictation.rs | 218 + desktop/src-tauri/src/inject.rs | 74 + desktop/src-tauri/src/lib.rs | 155 + desktop/src-tauri/src/main.rs | 5 + desktop/src-tauri/src/metrics.rs | 67 + desktop/src-tauri/src/settings.rs | 94 + desktop/src-tauri/src/ws.rs | 125 + desktop/src-tauri/tauri.conf.json | 90 + desktop/src/feedback/main.jsx | 237 + desktop/src/login/main.jsx | 160 + desktop/src/onboarding/main.jsx | 200 + desktop/src/overlay/main.jsx | 53 + desktop/src/settings/SettingsApp.jsx | 134 + desktop/src/settings/main.jsx | 5 + desktop/src/shared/tauri.js | 14 + desktop/src/shared/theme.js | 16 + desktop/src/tray/main.jsx | 66 + desktop/tray.html | 12 + desktop/vite.config.mjs | 29 + doc/backend-architecture.html | 412 ++ doc/frontend-design.html | 470 ++ doc/plan/design.html | 695 ++ ios/dudu/App/App.entitlements | 10 + ios/dudu/App/DictationController.swift | 553 ++ ios/dudu/App/DictationView.swift | 307 + ios/dudu/App/DuduApp.swift | 100 + ios/dudu/App/FeedbackView.swift | 304 + ios/dudu/App/HomeView.swift | 418 ++ ios/dudu/App/Info.plist | 44 + ios/dudu/App/LoginView.swift | 107 + ios/dudu/App/PayClient.swift | 20 + ios/dudu/App/PurchaseView.swift | 292 + ios/dudu/App/WechatAuth.swift | 19 + ios/dudu/Dudu.xcodeproj/project.pbxproj | 503 ++ .../contents.xcworkspacedata | 7 + ios/dudu/KeyboardExt/CommitController.swift | 68 + ios/dudu/KeyboardExt/Info.plist | 32 + ios/dudu/KeyboardExt/KeyboardExt.entitlements | 10 + ios/dudu/KeyboardExt/KeyboardView.swift | 280 + .../KeyboardExt/KeyboardViewController.swift | 182 + ios/dudu/Shared/AccountStore.swift | 148 + ios/dudu/Shared/ApiClient.swift | 517 ++ ios/dudu/Shared/DuduProtocol.swift | 101 + ios/dudu/Shared/DuduTheme.swift | 211 + ios/dudu/Shared/MetricsQueue.swift | 132 + ios/dudu/Shared/MockAsrDriver.swift | 46 + ios/dudu/Shared/PendingTextStore.swift | 46 + ios/dudu/Shared/WaveformView.swift | 32 + ios/dudu/project.yml | 60 + ios/spike-kb-mic/App/SpikeApp.swift | 60 + .../DuduKbMicSpike.xcodeproj/project.pbxproj | 419 ++ .../contents.xcworkspacedata | 7 + ios/spike-kb-mic/KeyboardExt/Info.plist | 32 + .../KeyboardExt/KeyboardViewController.swift | 77 + ios/spike-kb-mic/project.yml | 38 + server/cmd/gummycheck/main.go | 110 + server/cmd/server/main.go | 42 + server/docker-compose.yml | 20 + server/go.mod | 68 + server/go.sum | 171 + server/internal/asr/gummy.go | 221 + server/internal/asr/gummy_live_test.go | 95 + server/internal/asr/mock.go | 98 + server/internal/asr/mock_test.go | 44 + server/internal/asr/provider.go | 34 + server/internal/auth/handlers.go | 138 + server/internal/auth/jwt.go | 95 + server/internal/auth/jwt_test.go | 39 + server/internal/auth/wechat.go | 103 + server/internal/billing/billing.go | 251 + server/internal/config/config.go | 84 + server/internal/feedback/feedback.go | 146 + server/internal/gateway/gateway.go | 368 + server/internal/gateway/gateway_test.go | 239 + server/internal/httpapi/api_test.go | 316 + server/internal/httpapi/router.go | 100 + .../uploads/fb/fb1ec7a223d7cd478cab532a/1.png | Bin 0 -> 12 bytes .../uploads/fb/fb2da8604414454c20ac7984/1.png | Bin 0 -> 12 bytes .../uploads/fb/fb77426de8dfae40efbaaa6e/1.png | Bin 0 -> 12 bytes .../uploads/fb/fb9eeadc2527cd44e98e5dd4/1.png | Bin 0 -> 12 bytes server/internal/quota/quota.go | 173 + server/internal/quota/quota_test.go | 119 + server/internal/quota/reconcile.go | 67 + server/internal/store/models.go | 150 + server/internal/store/redis.go | 107 + server/internal/store/redis_test.go | 80 + server/internal/store/store.go | 41 + server/internal/telemetry/telemetry.go | 218 + server/internal/telemetry/telemetry_test.go | 55 + server/internal/user/handlers.go | 96 + server/pkg/protocol/dto.go | 139 + server/pkg/protocol/errors.go | 33 + server/pkg/protocol/ws.go | 63 + server/run-dev.sh | 12 + server/todo/todo.html | 482 ++ todo/for-me.html | 432 ++ todo/todo.html | 1404 ++++ todo/todo.json | 793 +++ web/index.html | 623 ++ 252 files changed, 40789 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 README.md create mode 100644 android/.gitignore create mode 100644 android/app/build.gradle.kts create mode 100644 android/app/proguard-rules.pro create mode 100644 android/app/src/main/AndroidManifest.xml create mode 100644 android/app/src/main/java/app/dudu/app/BuyScreen.kt create mode 100644 android/app/src/main/java/app/dudu/app/DuduApp.kt create mode 100644 android/app/src/main/java/app/dudu/app/FeedbackScreen.kt create mode 100644 android/app/src/main/java/app/dudu/app/LoginScreen.kt create mode 100644 android/app/src/main/java/app/dudu/app/MainActivity.kt create mode 100644 android/app/src/main/java/app/dudu/app/MineScreen.kt create mode 100644 android/app/src/main/java/app/dudu/app/OnboardingScreen.kt create mode 100644 android/app/src/main/java/app/dudu/app/Setup.kt create mode 100644 android/app/src/main/java/app/dudu/app/Ui.kt create mode 100644 android/app/src/main/java/app/dudu/audio/AudioStreamer.kt create mode 100644 android/app/src/main/java/app/dudu/data/AccountRepository.kt create mode 100644 android/app/src/main/java/app/dudu/data/MetricsFlushWorker.kt create mode 100644 android/app/src/main/java/app/dudu/data/MetricsQueue.kt create mode 100644 android/app/src/main/java/app/dudu/design/DText.kt create mode 100644 android/app/src/main/java/app/dudu/design/DuduIcons.kt create mode 100644 android/app/src/main/java/app/dudu/design/DuduPalette.kt create mode 100644 android/app/src/main/java/app/dudu/design/DuduTheme.kt create mode 100644 android/app/src/main/java/app/dudu/ime/DuduImeService.kt create mode 100644 android/app/src/main/java/app/dudu/ime/KeyboardController.kt create mode 100644 android/app/src/main/java/app/dudu/ime/ui/KeyboardScreen.kt create mode 100644 android/app/src/main/java/app/dudu/ime/ui/MicBar.kt create mode 100644 android/app/src/main/java/app/dudu/ime/ui/Waveform.kt create mode 100644 android/app/src/main/java/app/dudu/net/AsrSocket.kt create mode 100644 android/app/src/main/java/app/dudu/net/DuduApi.kt create mode 100644 android/app/src/main/java/app/dudu/pay/PayClient.kt create mode 100644 android/app/src/main/java/app/dudu/wechat/WechatAuth.kt create mode 100644 android/app/src/main/res/drawable/ic_launcher_foreground.xml create mode 100644 android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 android/app/src/main/res/values-night/colors.xml create mode 100644 android/app/src/main/res/values-night/themes.xml create mode 100644 android/app/src/main/res/values/colors.xml create mode 100644 android/app/src/main/res/values/strings.xml create mode 100644 android/app/src/main/res/values/themes.xml create mode 100644 android/app/src/main/res/xml/method.xml create mode 100644 android/build.gradle.kts create mode 100644 android/gradle.properties create mode 100644 android/gradle/libs.versions.toml create mode 100644 android/gradle/wrapper/gradle-wrapper.jar create mode 100644 android/gradle/wrapper/gradle-wrapper.properties create mode 100755 android/gradlew create mode 100644 android/gradlew.bat create mode 100644 android/settings.gradle.kts create mode 100644 design-pipeline/export-tokens.mjs create mode 100644 design-pipeline/generated/android/DuduTheme.kt create mode 100644 design-pipeline/generated/ios/DuduTheme.swift create mode 100644 design/SKILL.md create mode 100644 design/_adherence.oxlintrc.json create mode 100644 design/_ds_bundle.js create mode 100644 design/_ds_manifest.json create mode 100644 design/assets/app-icon-dark.svg create mode 100644 design/assets/app-icon.svg create mode 100644 design/assets/logo-mark-blue.svg create mode 100644 design/assets/logo-mark-ink.svg create mode 100644 design/assets/logo-mark-white.svg create mode 100644 design/assets/logo-mark.svg create mode 100644 design/assets/wordmark.svg create mode 100644 design/components/core/Badge.d.ts create mode 100644 design/components/core/Badge.jsx create mode 100644 design/components/core/Badge.prompt.md create mode 100644 design/components/core/Button.d.ts create mode 100644 design/components/core/Button.jsx create mode 100644 design/components/core/Button.prompt.md create mode 100644 design/components/core/Card.d.ts create mode 100644 design/components/core/Card.jsx create mode 100644 design/components/core/Card.prompt.md create mode 100644 design/components/core/IconButton.d.ts create mode 100644 design/components/core/IconButton.jsx create mode 100644 design/components/core/IconButton.prompt.md create mode 100644 design/components/core/Kbd.d.ts create mode 100644 design/components/core/Kbd.jsx create mode 100644 design/components/core/Kbd.prompt.md create mode 100644 design/components/core/ProgressBar.d.ts create mode 100644 design/components/core/ProgressBar.jsx create mode 100644 design/components/core/ProgressBar.prompt.md create mode 100644 design/components/core/core.card.html create mode 100644 design/components/forms/Input.d.ts create mode 100644 design/components/forms/Input.jsx create mode 100644 design/components/forms/Input.prompt.md create mode 100644 design/components/forms/SettingRow.d.ts create mode 100644 design/components/forms/SettingRow.jsx create mode 100644 design/components/forms/SettingRow.prompt.md create mode 100644 design/components/forms/Switch.d.ts create mode 100644 design/components/forms/Switch.jsx create mode 100644 design/components/forms/Switch.prompt.md create mode 100644 design/components/forms/forms.card.html create mode 100644 design/components/voice/MicBar.d.ts create mode 100644 design/components/voice/MicBar.jsx create mode 100644 design/components/voice/MicBar.prompt.md create mode 100644 design/components/voice/RecognitionOverlay.d.ts create mode 100644 design/components/voice/RecognitionOverlay.jsx create mode 100644 design/components/voice/RecognitionOverlay.prompt.md create mode 100644 design/components/voice/Waveform.d.ts create mode 100644 design/components/voice/Waveform.jsx create mode 100644 design/components/voice/Waveform.prompt.md create mode 100644 design/components/voice/voice.card.html create mode 100644 design/guidelines/brand-logo.card.html create mode 100644 design/guidelines/brand-wordmark.card.html create mode 100644 design/guidelines/colors-brand.card.html create mode 100644 design/guidelines/colors-neutrals.card.html create mode 100644 design/guidelines/colors-overlay.card.html create mode 100644 design/guidelines/colors-semantic.card.html create mode 100644 design/guidelines/colors-surfaces.card.html create mode 100644 design/guidelines/motion.card.html create mode 100644 design/guidelines/spacing-controls.card.html create mode 100644 design/guidelines/spacing-elevation.card.html create mode 100644 design/guidelines/spacing-grid.card.html create mode 100644 design/guidelines/spacing-radius.card.html create mode 100644 design/guidelines/type-families.card.html create mode 100644 design/guidelines/type-recognition.card.html create mode 100644 design/guidelines/type-scale.card.html create mode 100644 design/readme.md create mode 100644 design/styles.css create mode 100644 design/tokens/colors.css create mode 100644 design/tokens/effects.css create mode 100644 design/tokens/fonts.css create mode 100644 design/tokens/spacing.css create mode 100644 design/tokens/typography.css create mode 100644 design/ui_kits/desktop/DesktopShared.jsx create mode 100644 design/ui_kits/desktop/LoginPurchase.jsx create mode 100644 design/ui_kits/desktop/OverlayDemo.jsx create mode 100644 design/ui_kits/desktop/README.md create mode 100644 design/ui_kits/desktop/SettingsTray.jsx create mode 100644 design/ui_kits/desktop/index.html create mode 100644 design/ui_kits/mobile/AppScreens.jsx create mode 100644 design/ui_kits/mobile/KeyboardScreen.jsx create mode 100644 design/ui_kits/mobile/MobileShared.jsx create mode 100644 design/ui_kits/mobile/README.md create mode 100644 design/ui_kits/mobile/index.html create mode 100644 desktop/feedback.html create mode 100644 desktop/index.html create mode 100644 desktop/login.html create mode 100644 desktop/onboarding.html create mode 100644 desktop/overlay.html create mode 100644 desktop/package-lock.json create mode 100644 desktop/package.json create mode 100644 desktop/src-tauri/Cargo.lock create mode 100644 desktop/src-tauri/Cargo.toml create mode 100644 desktop/src-tauri/build.rs create mode 100644 desktop/src-tauri/capabilities/default.json create mode 100644 desktop/src-tauri/gen/schemas/acl-manifests.json create mode 100644 desktop/src-tauri/gen/schemas/capabilities.json create mode 100644 desktop/src-tauri/gen/schemas/desktop-schema.json create mode 100644 desktop/src-tauri/gen/schemas/macOS-schema.json create mode 100644 desktop/src-tauri/icons/icon.png create mode 100644 desktop/src-tauri/src/api.rs create mode 100644 desktop/src-tauri/src/audio.rs create mode 100644 desktop/src-tauri/src/dictation.rs create mode 100644 desktop/src-tauri/src/inject.rs create mode 100644 desktop/src-tauri/src/lib.rs create mode 100644 desktop/src-tauri/src/main.rs create mode 100644 desktop/src-tauri/src/metrics.rs create mode 100644 desktop/src-tauri/src/settings.rs create mode 100644 desktop/src-tauri/src/ws.rs create mode 100644 desktop/src-tauri/tauri.conf.json create mode 100644 desktop/src/feedback/main.jsx create mode 100644 desktop/src/login/main.jsx create mode 100644 desktop/src/onboarding/main.jsx create mode 100644 desktop/src/overlay/main.jsx create mode 100644 desktop/src/settings/SettingsApp.jsx create mode 100644 desktop/src/settings/main.jsx create mode 100644 desktop/src/shared/tauri.js create mode 100644 desktop/src/shared/theme.js create mode 100644 desktop/src/tray/main.jsx create mode 100644 desktop/tray.html create mode 100644 desktop/vite.config.mjs create mode 100644 doc/backend-architecture.html create mode 100644 doc/frontend-design.html create mode 100644 doc/plan/design.html create mode 100644 ios/dudu/App/App.entitlements create mode 100644 ios/dudu/App/DictationController.swift create mode 100644 ios/dudu/App/DictationView.swift create mode 100644 ios/dudu/App/DuduApp.swift create mode 100644 ios/dudu/App/FeedbackView.swift create mode 100644 ios/dudu/App/HomeView.swift create mode 100644 ios/dudu/App/Info.plist create mode 100644 ios/dudu/App/LoginView.swift create mode 100644 ios/dudu/App/PayClient.swift create mode 100644 ios/dudu/App/PurchaseView.swift create mode 100644 ios/dudu/App/WechatAuth.swift create mode 100644 ios/dudu/Dudu.xcodeproj/project.pbxproj create mode 100644 ios/dudu/Dudu.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 ios/dudu/KeyboardExt/CommitController.swift create mode 100644 ios/dudu/KeyboardExt/Info.plist create mode 100644 ios/dudu/KeyboardExt/KeyboardExt.entitlements create mode 100644 ios/dudu/KeyboardExt/KeyboardView.swift create mode 100644 ios/dudu/KeyboardExt/KeyboardViewController.swift create mode 100644 ios/dudu/Shared/AccountStore.swift create mode 100644 ios/dudu/Shared/ApiClient.swift create mode 100644 ios/dudu/Shared/DuduProtocol.swift create mode 100644 ios/dudu/Shared/DuduTheme.swift create mode 100644 ios/dudu/Shared/MetricsQueue.swift create mode 100644 ios/dudu/Shared/MockAsrDriver.swift create mode 100644 ios/dudu/Shared/PendingTextStore.swift create mode 100644 ios/dudu/Shared/WaveformView.swift create mode 100644 ios/dudu/project.yml create mode 100644 ios/spike-kb-mic/App/SpikeApp.swift create mode 100644 ios/spike-kb-mic/DuduKbMicSpike.xcodeproj/project.pbxproj create mode 100644 ios/spike-kb-mic/DuduKbMicSpike.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 ios/spike-kb-mic/KeyboardExt/Info.plist create mode 100644 ios/spike-kb-mic/KeyboardExt/KeyboardViewController.swift create mode 100644 ios/spike-kb-mic/project.yml create mode 100644 server/cmd/gummycheck/main.go create mode 100644 server/cmd/server/main.go create mode 100644 server/docker-compose.yml create mode 100644 server/go.mod create mode 100644 server/go.sum create mode 100644 server/internal/asr/gummy.go create mode 100644 server/internal/asr/gummy_live_test.go create mode 100644 server/internal/asr/mock.go create mode 100644 server/internal/asr/mock_test.go create mode 100644 server/internal/asr/provider.go create mode 100644 server/internal/auth/handlers.go create mode 100644 server/internal/auth/jwt.go create mode 100644 server/internal/auth/jwt_test.go create mode 100644 server/internal/auth/wechat.go create mode 100644 server/internal/billing/billing.go create mode 100644 server/internal/config/config.go create mode 100644 server/internal/feedback/feedback.go create mode 100644 server/internal/gateway/gateway.go create mode 100644 server/internal/gateway/gateway_test.go create mode 100644 server/internal/httpapi/api_test.go create mode 100644 server/internal/httpapi/router.go create mode 100644 server/internal/httpapi/var/uploads/fb/fb1ec7a223d7cd478cab532a/1.png create mode 100644 server/internal/httpapi/var/uploads/fb/fb2da8604414454c20ac7984/1.png create mode 100644 server/internal/httpapi/var/uploads/fb/fb77426de8dfae40efbaaa6e/1.png create mode 100644 server/internal/httpapi/var/uploads/fb/fb9eeadc2527cd44e98e5dd4/1.png create mode 100644 server/internal/quota/quota.go create mode 100644 server/internal/quota/quota_test.go create mode 100644 server/internal/quota/reconcile.go create mode 100644 server/internal/store/models.go create mode 100644 server/internal/store/redis.go create mode 100644 server/internal/store/redis_test.go create mode 100644 server/internal/store/store.go create mode 100644 server/internal/telemetry/telemetry.go create mode 100644 server/internal/telemetry/telemetry_test.go create mode 100644 server/internal/user/handlers.go create mode 100644 server/pkg/protocol/dto.go create mode 100644 server/pkg/protocol/errors.go create mode 100644 server/pkg/protocol/ws.go create mode 100755 server/run-dev.sh create mode 100644 server/todo/todo.html create mode 100644 todo/for-me.html create mode 100644 todo/todo.html create mode 100644 todo/todo.json create mode 100644 web/index.html diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..961b932 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,30 @@ +# CI 雏形:后端测试 + 令牌产物校验(git 仓库托管后生效) +name: ci +on: + push: + pull_request: + +jobs: + server: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: "1.26" + cache-dependency-path: server/go.sum + - name: Test + working-directory: server + run: | + go vet ./... + go test ./... + + design-tokens: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "22" + - name: Check generated tokens up to date + run: node design-pipeline/export-tokens.mjs --check diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9eb952a --- /dev/null +++ b/.gitignore @@ -0,0 +1,31 @@ +# macOS +.DS_Store + +# Node +node_modules/ +dist/ + +# Rust / Tauri +desktop/src-tauri/target/ + +# Android +android/.gradle/ +android/local.properties +android/app/build/ +android/build/ +*.apk + +# iOS / Xcode +ios/**/build/ +ios/**/DerivedData/ +ios/**/*.xcodeproj/xcuserdata/ +ios/**/*.xcodeproj/project.xcworkspace/xcuserdata/ + +# Go +server/var/ + +# 环境与密钥(统一走 Bitwarden/rbw,禁止入库) +.env +*.pem +*.p12 +*.mobileprovision diff --git a/README.md b/README.md new file mode 100644 index 0000000..cab8e3a --- /dev/null +++ b/README.md @@ -0,0 +1,49 @@ +# dudu(嘟嘟)语音输入法 + +付费语音输入法:大模型识别准确度 + 流式实时上屏 + 极简交互。全平台(Mac / Windows / iPhone / iPad / Android),桌面端按住全局快捷键说话,移动端键盘按住大麦克风说话。微信登录 + 微信支付时长包(不过期),每天免费试用 3 分钟。 + +## 仓库结构 + +``` +server/ Go 后端(gin + gorilla/websocket + PostgreSQL + Redis) + pkg/protocol/ 客户端协议常量与消息结构 —— 5 端对齐的唯一真相源 +desktop/ 桌面端 Tauri 2(Mac + Windows) +android/ Kotlin:主 App + IME Service +ios/ Swift:主 App + Keyboard Extension +design/ 官方设计系统(tokens / React 组件 / UI kits)—— 界面唯一真相源 +design-pipeline/ 令牌导出管线:tokens.css → DuduTheme.swift / DuduTheme.kt +doc/ 设计文档(HTML) +todo/ 项目 TODO(todo.html 看板,经 /todo skill 管理) +``` + +## 设计文档 + +| 文档 | 内容 | +|---|---| +| `doc/plan/design.html` | MVP 总体方案 v0.3(架构、里程碑、风险) | +| `doc/frontend-design.html` | 前端实现设计 v1.1(100% 还原设计系统) | +| `doc/backend-architecture.html` | 后端与数据架构 v1.1(API / 协议 / 计费账本) | + +## 开发 + +后端: + +```bash +cd server +docker compose up -d # postgres + redis +go run ./cmd/server # 默认 :8080,ASR_PROVIDER=mock +go test ./... +``` + +令牌导出(design/tokens 变更后执行并提交产物): + +```bash +node design-pipeline/export-tokens.mjs # 生成 +node design-pipeline/export-tokens.mjs --check # CI 校验 +``` + +## 关键约定 + +- 协议改动只改 `server/pkg/protocol`,各端对照实现 +- 界面规格以 `design/` 为准,硬规则见 `design/readme.md`(一屏一个蓝主按钮 / 无渐变 / 无 emoji / 浮层永远深色玻璃) +- 待办经 `/todo` 管理(看板 `todo/todo.html`),状态流转 open → doing → done →(用户验收)accepted diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 0000000..3e9c54c --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,7 @@ +.gradle/ +build/ +local.properties +.idea/ +*.iml +.DS_Store +.kotlin/ diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts new file mode 100644 index 0000000..231428e --- /dev/null +++ b/android/app/build.gradle.kts @@ -0,0 +1,64 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.compose) +} + +android { + namespace = "app.dudu" + compileSdk = 36 + + defaultConfig { + applicationId = "app.dudu" + minSdk = 26 + targetSdk = 36 + versionCode = 1 + versionName = "0.1.0" + + // 服务端地址(默认指向模拟器宿主机本地 server)。 + // TODO(release): 切生产 wss 域名后改为 wss://api.dudu.app/v1/asr/stream + buildConfigField("String", "WS_URL", "\"ws://10.0.2.2:8080/v1/asr/stream\"") + buildConfigField("String", "API_BASE", "\"http://10.0.2.2:8080\"") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + buildFeatures { + compose = true + buildConfig = true + } +} + +kotlin { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_17) + } +} + +dependencies { + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.activity.compose) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.lifecycle.viewmodel.ktx) + implementation(libs.androidx.savedstate.ktx) + + implementation(platform(libs.compose.bom)) + implementation(libs.compose.ui) + implementation(libs.compose.foundation) + + implementation(libs.okhttp) + implementation(libs.kotlinx.coroutines.android) + implementation(libs.androidx.work.runtime.ktx) +} diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro new file mode 100644 index 0000000..bb44455 --- /dev/null +++ b/android/app/proguard-rules.pro @@ -0,0 +1 @@ +# dudu Android:release 暂不混淆(isMinifyEnabled = false),此文件为占位。 diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..99f733d --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/java/app/dudu/app/BuyScreen.kt b/android/app/src/main/java/app/dudu/app/BuyScreen.kt new file mode 100644 index 0000000..d4701e6 --- /dev/null +++ b/android/app/src/main/java/app/dudu/app/BuyScreen.kt @@ -0,0 +1,288 @@ +package app.dudu.app + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import app.dudu.data.AccountRepository +import app.dudu.data.MetricsQueue +import app.dudu.design.DText +import app.dudu.design.LocalDuduPalette +import app.dudu.net.ApiException +import app.dudu.net.DuduApi +import app.dudu.pay.PayResult +import app.dudu.pay.payClient +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +/** 购买流程阶段。 */ +private enum class BuyPhase { PICK, PAYING, POLLING, SUCCESS } + +/** + * 购买时长页(13C):GET /v1/packs 渲染时长包 → POST /v1/orders(channel=app)→ + * PayClient(mock 2 秒成功)→ 轮询 GET /v1/orders/{id} 至 paid → 刷新 /v1/me → + * 成功态"已到账 · N 分钟"。 + */ +@Composable +fun BuyScreen(refreshKey: Int, onBack: () -> Unit) { + val p = LocalDuduPalette.current + val context = LocalContext.current + val repo = remember { AccountRepository.get(context) } + val metrics = remember { MetricsQueue.get(context) } + val account by repo.snapshot.collectAsState() + val scope = rememberCoroutineScope() + + var packs by remember { mutableStateOf?>(null) } + var packsError by remember { mutableStateOf(null) } + var selected by remember { mutableStateOf(null) } + var phase by remember { mutableStateOf(BuyPhase.PICK) } + var payError by remember { mutableStateOf(null) } + var paidMinutes by remember { mutableStateOf(0) } + + // 拉时长包(价格以接口返回为准,前端不写死) + LaunchedEffect(Unit) { + try { + val list = DuduApi.packs() + packs = list + // 默认选中带角标的档(无则第一档) + selected = list.firstOrNull { it.tag != null } ?: list.firstOrNull() + } catch (e: ApiException) { + packsError = e.message + } + } + + fun startPay() { + val pack = selected ?: return + val token = account.token ?: return + phase = BuyPhase.PAYING + payError = null + scope.launch { + try { + // 1. 下单拿 app 支付参数 + val order = DuduApi.createOrder(token, pack.id) + // 2. 拉起收银台(mock 2 秒后成功;TODO(wechat-pay) 换 OpenSDK) + when (val r = payClient.pay(order.orderId, order.payParams)) { + is PayResult.Cancelled -> { + phase = BuyPhase.PICK + payError = "已取消支付,可重新发起" + return@launch + } + is PayResult.Failed -> { + phase = BuyPhase.PICK + payError = "支付失败,稍后再试" + return@launch + } + is PayResult.Success -> Unit + } + // 3. 轮询订单态至 paid(1s 间隔,最长 30s) + phase = BuyPhase.POLLING + var paid = false + var attempts = 0 + while (attempts < 30) { + val st = DuduApi.orderStatus(token, order.orderId) + if (st.status == "paid") { + paid = true + break + } + if (st.status == "closed") { + phase = BuyPhase.PICK + payError = "订单已关闭,请重新下单" + return@launch + } + attempts++ + delay(1000) + } + if (!paid) { + phase = BuyPhase.PICK + payError = "支付确认中,稍后在我的页查看余额" + return@launch + } + // 4. 刷新 /v1/me 余额 + runCatching { + val me = DuduApi.me(token) + repo.applyMe( + userId = me.userId, + nicknameMasked = me.nicknameMasked, + balanceSeconds = me.balanceSeconds, + trialDailyLimit = me.trialDailyLimit, + trialUsedToday = me.trialUsedToday, + accountState = me.accountState, + ) + } + metrics.track("purchase.success", mapOf("pack_id" to pack.id)) + paidMinutes = pack.minutes + phase = BuyPhase.SUCCESS + } catch (e: ApiException) { + phase = BuyPhase.PICK + payError = e.message + } + } + } + + Column( + Modifier + .fillMaxSize() + .background(p.bgApp) + .systemBarsPadding() + .verticalScroll(rememberScrollState()) + .padding(horizontal = 24.dp), + ) { + TopBar("购买时长", onBack = if (phase == BuyPhase.SUCCESS) null else onBack) + VSpace(8) + + if (phase == BuyPhase.SUCCESS) { + // ── 成功态 ── + VSpace(64) + Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { + SuccessCheckCircle() + VSpace(16) + DText("已到账 · $paidMinutes 分钟", 17.sp, p.text1, weight = FontWeight.SemiBold) + VSpace(8) + DText("时长余额 ${account.balanceSeconds / 60} 分钟 · 不过期", 13.sp, p.text2) + VSpace(28) + GhostButton("完成", onClick = onBack) + } + VSpace(40) + return@Column + } + + // ── 余额卡 ── + DCard { + Row(verticalAlignment = Alignment.CenterVertically) { + Column(Modifier.weight(1f)) { + DText("时长余额", 13.sp, p.text3) + VSpace(4) + // TODO(font-outfit): 大数字应为 Outfit,暂用系统 mono + DText("${account.balanceSeconds / 60} 分钟", 24.sp, p.text1, weight = FontWeight.Bold, mono = true) + } + DText("时长不过期\n每天另有 ${account.trialDailyLimitSeconds / 60} 分钟免费试用", 12.sp, p.text3) + } + } + VSpace(16) + + // ── 时长包卡片 ── + when { + packsError != null -> { + DCard { + DText(packsError ?: "", 13.sp, p.danger) + VSpace(12) + GhostButton("重试") { + packsError = null + scope.launch { + try { + val list = DuduApi.packs() + packs = list + selected = list.firstOrNull { it.tag != null } ?: list.firstOrNull() + } catch (e: ApiException) { + packsError = e.message + } + } + } + } + } + packs == null -> DText("加载中", 13.sp, p.text3) + else -> { + packs?.forEach { pack -> + PackCard(pack, selected?.id == pack.id) { selected = pack } + VSpace(12) + } + } + } + + NoticeText(payError) + VSpace(16) + + // ── 主按钮(金额随选中联动)── + val pack = selected + val payText = when { + phase == BuyPhase.PAYING -> "正在拉起微信支付" + phase == BuyPhase.POLLING -> "等待支付确认" + pack != null -> "微信支付 ¥" + formatYuan(pack.priceCents) + else -> "微信支付" + } + PrimaryButton( + payText, + enabled = pack != null && account.token != null, + loading = phase == BuyPhase.PAYING || phase == BuyPhase.POLLING, + ) { startPay() } + VSpace(10) + DText( + "支付后立即到账 · 时长不过期", 12.sp, p.text3, + modifier = Modifier.fillMaxWidth(), + textAlign = androidx.compose.ui.text.style.TextAlign.Center, + ) + VSpace(40) + } +} + +/** 时长包卡片:分钟数 + 价格(数字突出)+ 单价注 + 省 x% 角标,选中 accent 边 + soft 底。 */ +@Composable +private fun PackCard(pack: DuduApi.Pack, selected: Boolean, onSelect: () -> Unit) { + val p = LocalDuduPalette.current + val src = remember { MutableInteractionSource() } + Row( + Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(14.dp)) + .background(if (selected) p.accentSoft else p.surfaceCard) + .border( + width = if (selected) 1.5.dp else 1.dp, + color = if (selected) p.accent else p.border1, + shape = RoundedCornerShape(14.dp), + ) + .clickable(interactionSource = src, indication = null, onClick = onSelect) + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Column(Modifier.weight(1f)) { + Row(verticalAlignment = Alignment.CenterVertically) { + DText("${pack.minutes} 分钟", 17.sp, p.text1, weight = FontWeight.SemiBold, mono = true) + pack.tag?.let { + HSpace(8) + Badge(it, BadgeKind.WARNING) + } + } + VSpace(4) + DText(pack.unitDesc, 12.sp, p.text3) + } + // TODO(font-outfit): 价格数字应为 Outfit,暂用系统 mono + DText("¥" + formatYuan(pack.priceCents), 20.sp, if (selected) p.accentText else p.text1, weight = FontWeight.Bold, mono = true) + } +} + +/** 分转元:900 → "9",3950 → "39.5"。 */ +private fun formatYuan(cents: Int): String { + val yuan = cents / 100 + val rem = cents % 100 + return when { + rem == 0 -> "$yuan" + rem % 10 == 0 -> "$yuan.${rem / 10}" + else -> "$yuan." + rem.toString().padStart(2, '0') + } +} diff --git a/android/app/src/main/java/app/dudu/app/DuduApp.kt b/android/app/src/main/java/app/dudu/app/DuduApp.kt new file mode 100644 index 0000000..3f8836a --- /dev/null +++ b/android/app/src/main/java/app/dudu/app/DuduApp.kt @@ -0,0 +1,15 @@ +package app.dudu.app + +import android.app.Application +import app.dudu.data.MetricsFlushWorker + +/** + * 应用入口:主 App 与 IME Service 同进程,这里做一次性初始化。 + * 目前只负责注册埋点周期上报(WorkManager)。 + */ +class DuduApp : Application() { + override fun onCreate() { + super.onCreate() + MetricsFlushWorker.schedule(this) + } +} diff --git a/android/app/src/main/java/app/dudu/app/FeedbackScreen.kt b/android/app/src/main/java/app/dudu/app/FeedbackScreen.kt new file mode 100644 index 0000000..39d8ed6 --- /dev/null +++ b/android/app/src/main/java/app/dudu/app/FeedbackScreen.kt @@ -0,0 +1,306 @@ +package app.dudu.app + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.net.Uri +import android.os.Build +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.PickVisualMediaRequest +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.graphics.PathEffect +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import app.dudu.BuildConfig +import app.dudu.data.AccountRepository +import app.dudu.design.DText +import app.dudu.design.IconPlus +import app.dudu.design.IconX +import app.dudu.design.LocalDuduPalette +import app.dudu.net.ApiException +import app.dudu.net.DuduApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.json.JSONObject + +private const val MAX_CONTENT = 1000 // protocol.FeedbackMaxContentLen +private const val MAX_IMAGES = 3 // protocol.FeedbackMaxImages +private const val MAX_IMAGE_BYTES = 5 shl 20 // protocol.FeedbackMaxImageBytes = 5MB + +/** 已选图片:原始字节 + 缩略图。 */ +private class PickedImage(val bytes: ByteArray, val thumb: Bitmap) + +/** + * 反馈页(13F):多行文本(1-1000 计数)+ 图片 ≤3 张 ≤5MB(PhotoPicker)+ + * 诊断信息开关 + multipart POST /v1/feedback;成功态"已收到 · 谢谢你"。 + */ +@Composable +fun FeedbackScreen(onBack: () -> Unit) { + val p = LocalDuduPalette.current + val context = LocalContext.current + val repo = remember { AccountRepository.get(context) } + val account by repo.snapshot.collectAsState() + val scope = rememberCoroutineScope() + + var content by remember { mutableStateOf("") } + var images by remember { mutableStateOf(listOf()) } + var withDiagnostics by remember { mutableStateOf(true) } + var submitting by remember { mutableStateOf(false) } + var done by remember { mutableStateOf(false) } + var error by remember { mutableStateOf(null) } + + val picker = rememberLauncherForActivityResult( + ActivityResultContracts.PickMultipleVisualMedia(MAX_IMAGES), + ) { uris -> + if (uris.isEmpty()) return@rememberLauncherForActivityResult + scope.launch { + val room = MAX_IMAGES - images.size + var oversize = false + val loaded = withContext(Dispatchers.IO) { + uris.take(room).mapNotNull { uri -> + val bytes = readBytes(context, uri) ?: return@mapNotNull null + if (bytes.size > MAX_IMAGE_BYTES) { + oversize = true + return@mapNotNull null + } + decodeThumb(bytes)?.let { PickedImage(bytes, it) } + } + } + images = images + loaded + error = if (oversize) "单张图片需小于 5MB" else null + } + } + + fun submit() { + val token = account.token ?: run { error = "请先登录"; return } + if (submitting || content.isBlank()) return + submitting = true + error = null + scope.launch { + try { + val diag = if (withDiagnostics) diagnosticsJson(context) else null + DuduApi.submitFeedback( + token = token, + content = content.trim(), + images = images.map { it.bytes }, + diagnostics = diag, + ) + done = true + } catch (e: ApiException) { + error = if (e.code == "FEEDBACK_RATE_LIMITED") "今天反馈次数已达上限" else e.message + } finally { + submitting = false + } + } + } + + Column( + Modifier + .fillMaxSize() + .background(p.bgApp) + .systemBarsPadding() + .verticalScroll(rememberScrollState()) + .padding(horizontal = 24.dp), + ) { + TopBar("反馈问题", onBack) + VSpace(8) + + if (done) { + // ── 成功态 ── + VSpace(64) + Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { + SuccessCheckCircle() + VSpace(16) + DText("已收到 · 谢谢你", 17.sp, p.text1, weight = FontWeight.SemiBold) + VSpace(28) + GhostButton("继续反馈") { + content = "" + images = emptyList() + done = false + } + VSpace(12) + GhostButton("返回", onClick = onBack) + } + VSpace(40) + return@Column + } + + // ── 文本输入 ── + DCard { + BasicTextField( + value = content, + onValueChange = { if (it.length <= MAX_CONTENT) content = it }, + modifier = Modifier + .fillMaxWidth() + .height(140.dp), + textStyle = TextStyle(color = p.text1, fontSize = 15.sp, lineHeight = 22.sp), + cursorBrush = SolidColor(p.accent), + decorationBox = { inner -> + Box { + if (content.isEmpty()) { + DText("说说遇到的问题或建议", 15.sp, p.text3) + } + inner() + } + }, + ) + Row(Modifier.fillMaxWidth()) { + Spacer(Modifier.weight(1f)) + DText("${content.length} / $MAX_CONTENT", 12.sp, p.text3, mono = true) + } + } + VSpace(12) + + // ── 图片 ── + Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) { + images.forEachIndexed { i, img -> + Box { + Image( + bitmap = img.thumb.asImageBitmap(), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .size(64.dp) + .clip(RoundedCornerShape(6.dp)), + ) + val src = remember { MutableInteractionSource() } + Box( + Modifier + .size(20.dp) + .align(Alignment.TopEnd) + .offset(x = 6.dp, y = (-6).dp) + .clip(CircleShape) + .background(p.overlayBg) + .clickable(interactionSource = src, indication = null) { + images = images.filterIndexed { idx, _ -> idx != i } + }, + contentAlignment = Alignment.Center, + ) { + IconX(10.dp, p.overlayText) + } + } + } + if (images.size < MAX_IMAGES) { + val src = remember { MutableInteractionSource() } + val borderColor = p.border2 + Box( + Modifier + .size(64.dp) + .clip(RoundedCornerShape(6.dp)) + .drawBehind { + drawRoundRect( + color = borderColor, + cornerRadius = CornerRadius(6.dp.toPx()), + style = Stroke( + width = 2.dp.toPx(), + pathEffect = PathEffect.dashPathEffect(floatArrayOf(8f, 8f)), + ), + ) + } + .clickable(interactionSource = src, indication = null) { + picker.launch( + PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly), + ) + }, + contentAlignment = Alignment.Center, + ) { + IconPlus(20.dp, p.text3) + } + } + } + VSpace(6) + DText("最多 3 张 · 单张 5MB 以内", 12.sp, p.text3) + VSpace(16) + + // ── 诊断信息开关 ── + DCard { + Row(verticalAlignment = Alignment.CenterVertically) { + Column(Modifier.weight(1f)) { + DText("附带诊断信息", 14.sp, p.text1) + VSpace(3) + DText("版本、系统、设备型号", 12.sp, p.text3) + } + DSwitch(withDiagnostics) { withDiagnostics = !withDiagnostics } + } + } + + NoticeText(error) + VSpace(20) + + PrimaryButton( + "提交", + enabled = content.isNotBlank(), + loading = submitting, + ) { submit() } + VSpace(40) + } +} + +// ─── 工具 ──────────────────────────────────────────────────────────────────── + +private fun readBytes(context: Context, uri: Uri): ByteArray? = + runCatching { + context.contentResolver.openInputStream(uri)?.use { it.readBytes() } + }.getOrNull() + +/** 缩略图:inSampleSize 解到 ~128px,避免大图占内存。 */ +private fun decodeThumb(bytes: ByteArray): Bitmap? { + val bounds = BitmapFactory.Options().apply { inJustDecodeBounds = true } + BitmapFactory.decodeByteArray(bytes, 0, bytes.size, bounds) + if (bounds.outWidth <= 0 || bounds.outHeight <= 0) return null + var sample = 1 + while (bounds.outWidth / (sample * 2) >= 128 && bounds.outHeight / (sample * 2) >= 128) sample *= 2 + val opts = BitmapFactory.Options().apply { inSampleSize = sample } + return BitmapFactory.decodeByteArray(bytes, 0, bytes.size, opts) +} + +/** 诊断信息(不含识别文本):app 版本 / Android 版本 / 设备型号 / device_id。 */ +private fun diagnosticsJson(context: Context): String = + JSONObject() + .put("platform", "android") + .put("app_version", BuildConfig.VERSION_NAME) + .put("os_version", "Android " + Build.VERSION.RELEASE) + .put("device_model", Build.MANUFACTURER + " " + Build.MODEL) + .put("device_id", AccountRepository.get(context).deviceId) + .toString() diff --git a/android/app/src/main/java/app/dudu/app/LoginScreen.kt b/android/app/src/main/java/app/dudu/app/LoginScreen.kt new file mode 100644 index 0000000..56e5fa4 --- /dev/null +++ b/android/app/src/main/java/app/dudu/app/LoginScreen.kt @@ -0,0 +1,154 @@ +package app.dudu.app + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsPressedAsState +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import app.dudu.BuildConfig +import app.dudu.data.AccountRepository +import app.dudu.data.AccountState +import app.dudu.data.MetricsQueue +import app.dudu.design.DText +import app.dudu.design.DuduLogoMark +import app.dudu.design.LocalDuduPalette +import app.dudu.net.ApiException +import app.dudu.net.DuduApi +import app.dudu.wechat.wechatAuth +import kotlinx.coroutines.launch + +/** 微信绿——全局唯一例外色(仅登录按钮,见设计核查清单)。 */ +private val WechatGreen = Color(0xFF07C160) +private val WechatGreenPress = Color(0xFF06AD56) + +/** + * 登录页(13B):居中字标 + 底部微信绿胶囊按钮。 + * 登录流:WechatAuth.requestCode()(mock)→ POST /v1/auth/wechat → 存 JWT → + * GET /v1/me 刷新余额 → 回调 onLoggedIn。 + */ +@Composable +fun LoginScreen(onLoggedIn: () -> Unit, onBack: () -> Unit) { + val p = LocalDuduPalette.current + val context = LocalContext.current + val repo = remember { AccountRepository.get(context) } + val metrics = remember { MetricsQueue.get(context) } + val scope = rememberCoroutineScope() + + var loading by remember { mutableStateOf(false) } + var error by remember { mutableStateOf(null) } + + fun doLogin() { + if (loading) return + loading = true + error = null + scope.launch { + try { + val code = wechatAuth.requestCode() // TODO(wechat-sdk): mock → OpenSDK + val login = DuduApi.loginWechat(code) + repo.setLogin(login.token, login.user.userId, login.user.nicknameMasked) + val me = DuduApi.me(login.token) + repo.applyMe( + userId = me.userId, + nicknameMasked = me.nicknameMasked, + balanceSeconds = me.balanceSeconds, + trialDailyLimit = me.trialDailyLimit, + trialUsedToday = me.trialUsedToday, + accountState = me.accountState, + ) + metrics.track("login.success") + onLoggedIn() + } catch (e: ApiException) { + error = e.message + } finally { + loading = false + } + } + } + + Column( + Modifier + .fillMaxSize() + .background(p.bgApp) + .systemBarsPadding() + .padding(horizontal = 24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Box(Modifier.padding(top = 4.dp).fillMaxWidth()) { TopBar("", onBack) } + Spacer(Modifier.weight(1f)) + + DuduLogoMark(64.dp, p.accent) + Spacer(Modifier.height(16.dp)) + // TODO(font-outfit): 字标应为 Outfit 字体,拿到字体文件前用系统默认 + 加宽字距 + DText("dudu", 24.sp, p.text1, weight = FontWeight.SemiBold) + Spacer(Modifier.height(8.dp)) + DText("大模型语音输入 · 说话即上屏", 14.sp, p.text2) + + Spacer(Modifier.weight(1f)) + + NoticeText(error) + Spacer(Modifier.height(14.dp)) + + // 微信绿胶囊按钮(56dp,唯一例外色) + run { + val src = remember { MutableInteractionSource() } + val pressed by src.collectIsPressedAsState() + Box( + Modifier + .fillMaxWidth() + .height(56.dp) + .alpha(if (loading) 0.6f else 1f) + .clip(RoundedCornerShape(999.dp)) + .background(if (pressed) WechatGreenPress else WechatGreen) + .clickable(interactionSource = src, indication = null, enabled = !loading) { doLogin() }, + contentAlignment = Alignment.Center, + ) { + DText(if (loading) "登录中" else "微信一键登录", 16.sp, Color.White, weight = FontWeight.Medium) + } + } + Spacer(Modifier.height(12.dp)) + DText( + "登录即同意《用户协议》与《隐私政策》", 12.sp, p.text3, + modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Center, + ) + + // 离线 mock 开关:后端不可用时切假登录态联调键盘(仅 debug 包) + if (BuildConfig.DEBUG) { + Spacer(Modifier.height(16.dp)) + val src = remember { MutableInteractionSource() } + DText( + "离线联调 · 跳过登录", 12.sp, p.text3, + modifier = Modifier + .clickable(interactionSource = src, indication = null) { + repo.setMockState(AccountState.TRIAL) + onLoggedIn() + } + .padding(8.dp), + ) + } + Spacer(Modifier.height(24.dp)) + } +} diff --git a/android/app/src/main/java/app/dudu/app/MainActivity.kt b/android/app/src/main/java/app/dudu/app/MainActivity.kt new file mode 100644 index 0000000..f56572e --- /dev/null +++ b/android/app/src/main/java/app/dudu/app/MainActivity.kt @@ -0,0 +1,112 @@ +package app.dudu.app + +import android.content.Intent +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.BackHandler +import androidx.activity.compose.setContent +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import app.dudu.data.AccountRepository +import app.dudu.data.AccountState +import app.dudu.design.DuduDesign + +/** 主 App 路由(简单状态路由,页面少且层级浅,不引 Navigation 依赖)。 */ +enum class Route { ONBOARDING, LOGIN, MINE, BUY, FEEDBACK } + +/** + * 主 App(13B/13C/13F): + * - 启用引导三步(输入法 / 麦克风 / 切换试用),完成进我的页 + * - 微信登录(mock OpenSDK,见 wechat/WechatAuth.kt) + * - 我的页(账户卡 / 试用进度 / 权限自检 / 入口) + * - 购买时长(packs / 订单 / mock 微信支付,见 pay/PayClient.kt) + * - 反馈问题(multipart 提交) + * 键盘经 EXTRA_DEST 深链进入对应页(login / buy / permission / home)。 + */ +class MainActivity : ComponentActivity() { + + private var resumeTick by mutableIntStateOf(0) + private var route by mutableStateOf(Route.MINE) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + route = routeFor(intent.getStringExtra(EXTRA_DEST)) + setContent { + DuduDesign { + Root( + route = route, + dest = intent.getStringExtra(EXTRA_DEST), + refreshKey = resumeTick, + onNavigate = { route = it }, + ) + } + } + } + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + setIntent(intent) + route = routeFor(intent.getStringExtra(EXTRA_DEST)) + } + + override fun onResume() { + super.onResume() + resumeTick++ // 从系统设置返回后触发重查启用/权限状态 + } + + /** 键盘深链 → 落地页;冷启动按引导完成度决定。 */ + private fun routeFor(dest: String?): Route { + val guest = AccountRepository.get(this).snapshot.value.state == AccountState.GUEST + return when (dest) { + "login" -> Route.LOGIN + "buy" -> if (guest) Route.LOGIN else Route.BUY + "permission" -> Route.ONBOARDING + else -> if (setupComplete(this)) Route.MINE else Route.ONBOARDING + } + } + + companion object { + const val EXTRA_DEST = "dest" // login | buy | permission | home + } +} + +@Composable +private fun Root( + route: Route, + dest: String?, + refreshKey: Int, + onNavigate: (Route) -> Unit, +) { + // 二级页返回我的页(我的/引导页交给系统默认返回 = 退出) + if (route == Route.LOGIN || route == Route.BUY || route == Route.FEEDBACK) { + BackHandler { onNavigate(Route.MINE) } + } + + when (route) { + Route.ONBOARDING -> OnboardingScreen( + dest = dest, + refreshKey = refreshKey, + onDone = { onNavigate(Route.MINE) }, + ) + Route.LOGIN -> LoginScreen( + onLoggedIn = { onNavigate(if (dest == "buy") Route.BUY else Route.MINE) }, + onBack = { onNavigate(Route.MINE) }, + ) + Route.MINE -> MineScreen( + refreshKey = refreshKey, + onLogin = { onNavigate(Route.LOGIN) }, + onBuy = { onNavigate(Route.BUY) }, + onFeedback = { onNavigate(Route.FEEDBACK) }, + ) + Route.BUY -> BuyScreen( + refreshKey = refreshKey, + onBack = { onNavigate(Route.MINE) }, + ) + Route.FEEDBACK -> FeedbackScreen( + onBack = { onNavigate(Route.MINE) }, + ) + } +} diff --git a/android/app/src/main/java/app/dudu/app/MineScreen.kt b/android/app/src/main/java/app/dudu/app/MineScreen.kt new file mode 100644 index 0000000..bfc267b --- /dev/null +++ b/android/app/src/main/java/app/dudu/app/MineScreen.kt @@ -0,0 +1,248 @@ +package app.dudu.app + +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.provider.Settings +import android.view.inputmethod.InputMethodManager +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import app.dudu.BuildConfig +import app.dudu.data.AccountRepository +import app.dudu.data.AccountState +import app.dudu.design.DText +import app.dudu.design.IconCheck +import app.dudu.design.IconUser +import app.dudu.design.LocalDuduPalette +import app.dudu.net.ApiException +import app.dudu.net.DuduApi +import kotlinx.coroutines.launch + +/** + * 我的页(13B):账户卡 / 试用进度 / 权限自检 / 入口(购买、反馈、退出)。 + * 未登录 = 游客态:试用剩余 + 登录引导。 + */ +@Composable +fun MineScreen( + refreshKey: Int, + onLogin: () -> Unit, + onBuy: () -> Unit, + onFeedback: () -> Unit, +) { + val p = LocalDuduPalette.current + val context = LocalContext.current + val repo = remember { AccountRepository.get(context) } + val account by repo.snapshot.collectAsState() + val scope = rememberCoroutineScope() + + val imeEnabled = remember(refreshKey) { isImeEnabled(context) } + val imeSelected = remember(refreshKey) { isImeSelected(context) } + val micGranted = remember(refreshKey) { hasMicPermission(context) } + + // 进入页面 / 回前台时刷新 /v1/me(登录态下;离线 mock 会话不发请求) + LaunchedEffect(refreshKey, account.token) { + val token = account.token ?: return@LaunchedEffect + if (repo.offlineMock) return@LaunchedEffect + try { + val me = DuduApi.me(token) + repo.applyMe( + userId = me.userId, + nicknameMasked = me.nicknameMasked, + balanceSeconds = me.balanceSeconds, + trialDailyLimit = me.trialDailyLimit, + trialUsedToday = me.trialUsedToday, + accountState = me.accountState, + ) + } catch (e: ApiException) { + if (e.code == "UNAUTHORIZED") repo.markUnauthorized() + // 网络失败静默:展示本地缓存 + } + } + + Column( + Modifier + .fillMaxSize() + .background(p.bgApp) + .systemBarsPadding() + .verticalScroll(rememberScrollState()) + .padding(horizontal = 24.dp), + ) { + TopBar("我的", onBack = null) + VSpace(8) + + if (account.state == AccountState.GUEST) { + // ── 游客态 ── + DCard { + Row(verticalAlignment = Alignment.CenterVertically) { + AvatarCircle() + HSpace(12) + Column { + DText("未登录", 16.sp, p.text1, weight = FontWeight.Medium) + VSpace(4) + DText("登录后购买时长,余额不过期", 13.sp, p.text3) + } + } + VSpace(16) + PrimaryButton("微信登录", onClick = onLogin) + } + VSpace(12) + TrialCard(account.trialUsedTodaySeconds, account.trialDailyLimitSeconds) + } else { + // ── 登录态:账户卡 ── + DCard { + Row(verticalAlignment = Alignment.CenterVertically) { + AvatarCircle() + HSpace(12) + Column(Modifier.weight(1f)) { + DText(account.nicknameMasked ?: "dudu 用户", 16.sp, p.text1, weight = FontWeight.Medium) + VSpace(4) + DText("时长余额 ${account.balanceSeconds / 60} 分钟 · 不过期", 13.sp, p.text2) + } + when (account.state) { + AccountState.OK -> Badge("余额充足", BadgeKind.POSITIVE) + AccountState.TRIAL -> Badge("试用中", BadgeKind.NEUTRAL) + else -> Badge("余额不足", BadgeKind.WARNING) + } + } + } + VSpace(12) + TrialCard(account.trialUsedTodaySeconds, account.trialDailyLimitSeconds) + } + + VSpace(12) + + // ── 权限自检 ── + DCard { + DText("权限自检", 13.sp, p.text3) + VSpace(6) + CheckRow("输入法已启用", imeEnabled) { + context.startActivity(Intent(Settings.ACTION_INPUT_METHOD_SETTINGS)) + } + Divider1() + CheckRow("已选为当前输入法", imeSelected) { + val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + imm.showInputMethodPicker() + } + Divider1() + CheckRow("麦克风权限", micGranted) { + context.startActivity( + Intent( + Settings.ACTION_APPLICATION_DETAILS_SETTINGS, + Uri.fromParts("package", context.packageName, null), + ), + ) + } + } + + VSpace(12) + + // ── 入口 ── + DCard { + ListRow("购买时长") { if (account.state == AccountState.GUEST) onLogin() else onBuy() } + Divider1() + ListRow("反馈问题") { if (account.state == AccountState.GUEST) onLogin() else onFeedback() } + if (account.state != AccountState.GUEST) { + Divider1() + ListRow("退出登录", danger = true) { + val token = account.token + repo.logout() + if (token != null) scope.launch { DuduApi.logout(token) } + } + } + } + + // ── 离线 mock 开关(debug 包):无后端时手动切四态联调键盘 ── + if (BuildConfig.DEBUG) { + VSpace(28) + DText("账户状态 · 离线联调", 13.sp, p.text3) + VSpace(8) + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + StateChip("ok", account.state == AccountState.OK) { repo.setMockState(AccountState.OK) } + StateChip("trial", account.state == AccountState.TRIAL) { repo.setMockState(AccountState.TRIAL) } + StateChip("guest", account.state == AccountState.GUEST) { repo.setMockState(AccountState.GUEST) } + StateChip("quota", account.state == AccountState.QUOTA) { repo.setMockState(AccountState.QUOTA) } + } + } + VSpace(40) + } +} + +@Composable +private fun AvatarCircle() { + val p = LocalDuduPalette.current + Box( + Modifier + .size(44.dp) + .clip(CircleShape) + .background(p.accentSoft), + contentAlignment = Alignment.Center, + ) { + IconUser(22.dp, p.accentText) + } +} + +/** 今日免费试用卡:用量 mono + 进度条。 */ +@Composable +private fun TrialCard(usedSeconds: Int, limitSeconds: Int) { + val p = LocalDuduPalette.current + val limit = if (limitSeconds > 0) limitSeconds else 180 + DCard { + Row(verticalAlignment = Alignment.CenterVertically) { + DText("今日免费试用", 14.sp, p.text1) + Spacer(Modifier.weight(1f)) + DText("${usedSeconds / 60} / ${limit / 60} 分钟", 13.sp, p.text2, mono = true) + } + VSpace(10) + DProgressBar(usedSeconds.toFloat() / limit) + } +} + +/** 权限自检行:正常绿对勾,异常红色"去开启 ›"。 */ +@Composable +private fun CheckRow(title: String, ok: Boolean, onFix: () -> Unit) { + val p = LocalDuduPalette.current + val src = remember { MutableInteractionSource() } + Row( + Modifier + .fillMaxWidth() + .clickable(interactionSource = src, indication = null, enabled = !ok, onClick = onFix) + .padding(vertical = 13.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + DText(title, 14.sp, p.text1) + Spacer(Modifier.weight(1f)) + if (ok) { + IconCheck(15.dp, p.positive) + } else { + DText("去开启 ›", 13.sp, p.danger) + } + } +} diff --git a/android/app/src/main/java/app/dudu/app/OnboardingScreen.kt b/android/app/src/main/java/app/dudu/app/OnboardingScreen.kt new file mode 100644 index 0000000..5bc4ed2 --- /dev/null +++ b/android/app/src/main/java/app/dudu/app/OnboardingScreen.kt @@ -0,0 +1,183 @@ +package app.dudu.app + +import android.Manifest +import android.content.Context +import android.content.Intent +import android.provider.Settings +import android.view.inputmethod.InputMethodManager +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import app.dudu.data.MetricsQueue +import app.dudu.design.DText +import app.dudu.design.DuduLogoMark +import app.dudu.design.IconCheck +import app.dudu.design.LocalDuduPalette + +/** + * 启用引导三步(13B):启用输入法 → 麦克风权限 → 切到 dudu 试一试。 + * 每步完成自动勾选(onResume 重查系统状态),全部完成后主按钮转"完成"进我的页。 + */ +@Composable +fun OnboardingScreen( + dest: String?, + refreshKey: Int, + onDone: () -> Unit, +) { + val p = LocalDuduPalette.current + val context = LocalContext.current + val metrics = remember { MetricsQueue.get(context) } + + // refreshKey 变化(onResume)时重查系统状态 + val imeEnabled = remember(refreshKey) { isImeEnabled(context) } + val imeSelected = remember(refreshKey) { isImeSelected(context) } + var micGranted by remember(refreshKey) { mutableStateOf(hasMicPermission(context)) } + + val permissionLauncher = rememberLauncherForActivityResult( + ActivityResultContracts.RequestPermission(), + ) { granted -> micGranted = granted } + + val step = when { + !imeEnabled -> 0 + !micGranted -> 1 + !imeSelected -> 2 + else -> 3 // 全部完成 + } + + // 漏斗埋点:步骤推进 + 键盘启用(一次性) + LaunchedEffect(step) { metrics.track("onboarding.step", mapOf("step" to step)) } + LaunchedEffect(imeEnabled) { + if (imeEnabled) metrics.trackOnce("kb_enabled", "kb.enabled") + } + + Column( + Modifier + .fillMaxSize() + .background(p.bgApp) + .systemBarsPadding() + .verticalScroll(rememberScrollState()) + .padding(horizontal = 24.dp), + ) { + Spacer(Modifier.height(40.dp)) + DuduLogoMark(44.dp, p.accent) + Spacer(Modifier.height(16.dp)) + DText("三步开启 dudu", 20.sp, p.text1, weight = FontWeight.SemiBold) + Spacer(Modifier.height(6.dp)) + DText("按住说话,松开上屏", 14.sp, p.text2) + Spacer(Modifier.height(24.dp)) + + // 从键盘跳来的场景提示 + when (dest) { + "permission" -> { ContextBanner("键盘请求开启麦克风权限"); Spacer(Modifier.height(20.dp)) } + else -> Unit + } + + StepCard( + index = 1, title = "启用输入法", desc = "在系统设置中开启 dudu 语音输入", + done = imeEnabled, active = step == 0, + ) + Spacer(Modifier.height(12.dp)) + StepCard( + index = 2, title = "开启麦克风权限", desc = "语音识别需要使用麦克风", + done = micGranted, active = step == 1, + ) + Spacer(Modifier.height(12.dp)) + StepCard( + index = 3, title = "试一试", desc = "切到 dudu 键盘,按住说话", + done = imeSelected, active = step == 2, + ) + + Spacer(Modifier.height(28.dp)) + + // 每屏唯一蓝色主按钮,文案随步骤 + when (step) { + 0 -> PrimaryButton("去设置") { + context.startActivity(Intent(Settings.ACTION_INPUT_METHOD_SETTINGS)) + } + 1 -> PrimaryButton("开启麦克风权限") { + permissionLauncher.launch(Manifest.permission.RECORD_AUDIO) + } + 2 -> PrimaryButton("切换到 dudu 键盘") { + val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + imm.showInputMethodPicker() + } + else -> PrimaryButton("完成", onClick = onDone) + } + + if (step in 1..2) { + Spacer(Modifier.height(12.dp)) + GhostButton("先跳过,去我的页", onClick = onDone) + } + Spacer(Modifier.height(40.dp)) + } +} + +/** 三步卡片:done(绿对勾 + positive-soft 图标底)/ active(1.5px accent 边)/ todo(opacity .62)。 */ +@Composable +private fun StepCard(index: Int, title: String, desc: String, done: Boolean, active: Boolean) { + val p = LocalDuduPalette.current + Row( + Modifier + .fillMaxWidth() + .alpha(if (!done && !active) 0.62f else 1f) + .clip(RoundedCornerShape(14.dp)) + .background(p.surfaceCard) + .border( + width = if (active) 1.5.dp else 1.dp, + color = if (active) p.accent else p.border1, + shape = RoundedCornerShape(14.dp), + ) + .padding(16.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Box( + Modifier + .size(28.dp) + .clip(CircleShape) + .background(if (done) p.positiveSoft else p.surface2), + contentAlignment = Alignment.Center, + ) { + if (done) { + IconCheck(14.dp, p.positive) + } else { + DText("$index", 13.sp, if (active) p.accentText else p.text3, weight = FontWeight.Medium) + } + } + Column { + DText(title, 15.sp, p.text1, weight = FontWeight.Medium) + Spacer(Modifier.height(2.dp)) + DText(desc, 12.sp, p.text3) + } + } +} diff --git a/android/app/src/main/java/app/dudu/app/Setup.kt b/android/app/src/main/java/app/dudu/app/Setup.kt new file mode 100644 index 0000000..af6da6b --- /dev/null +++ b/android/app/src/main/java/app/dudu/app/Setup.kt @@ -0,0 +1,31 @@ +package app.dudu.app + +import android.Manifest +import android.content.Context +import android.content.pm.PackageManager +import android.provider.Settings +import androidx.core.content.ContextCompat + +/** 系统状态自检:输入法启用 / 选为当前 / 麦克风权限(引导页与我的页共用)。 */ + +fun isImeEnabled(context: Context): Boolean { + val enabled = Settings.Secure.getString( + context.contentResolver, Settings.Secure.ENABLED_INPUT_METHODS, + ) ?: return false + return enabled.split(':').any { it.startsWith(context.packageName + "/") } +} + +fun isImeSelected(context: Context): Boolean { + val current = Settings.Secure.getString( + context.contentResolver, Settings.Secure.DEFAULT_INPUT_METHOD, + ) ?: return false + return current.startsWith(context.packageName + "/") +} + +fun hasMicPermission(context: Context): Boolean = + ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO) == + PackageManager.PERMISSION_GRANTED + +/** 三步引导是否全部完成(决定冷启动落引导页还是我的页)。 */ +fun setupComplete(context: Context): Boolean = + isImeEnabled(context) && hasMicPermission(context) diff --git a/android/app/src/main/java/app/dudu/app/Ui.kt b/android/app/src/main/java/app/dudu/app/Ui.kt new file mode 100644 index 0000000..3cbad97 --- /dev/null +++ b/android/app/src/main/java/app/dudu/app/Ui.kt @@ -0,0 +1,299 @@ +package app.dudu.app + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsPressedAsState +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import app.dudu.design.DText +import app.dudu.design.IconCheck +import app.dudu.design.IconChevronLeft +import app.dudu.design.IconChevronRight +import app.dudu.design.LocalDuduPalette + +/** 主 App 共用组件:按钮 / 卡片 / 列表行 / 顶栏 / 进度条 / 开关,颜色只取 DuduPalette。 */ + +/** 每屏唯一蓝色主按钮(44dp,圆角 6)。 */ +@Composable +fun PrimaryButton( + text: String, + modifier: Modifier = Modifier, + enabled: Boolean = true, + loading: Boolean = false, + onClick: () -> Unit, +) { + val p = LocalDuduPalette.current + val src = remember { MutableInteractionSource() } + val pressed by src.collectIsPressedAsState() + val clickable = enabled && !loading + Box( + modifier + .fillMaxWidth() + .height(44.dp) + .alpha(if (clickable) 1f else 0.55f) + .clip(RoundedCornerShape(6.dp)) + .background(if (pressed && clickable) p.accentPress else p.accent) + .clickable(interactionSource = src, indication = null, enabled = clickable, onClick = onClick), + contentAlignment = Alignment.Center, + ) { + DText(if (loading) "请稍候" else text, 15.sp, p.textOnAccent, weight = FontWeight.Medium) + } +} + +/** ghost 次按钮:边框 + 文字色。 */ +@Composable +fun GhostButton(text: String, modifier: Modifier = Modifier, onClick: () -> Unit) { + val p = LocalDuduPalette.current + val src = remember { MutableInteractionSource() } + val pressed by src.collectIsPressedAsState() + Box( + modifier + .fillMaxWidth() + .height(44.dp) + .clip(RoundedCornerShape(6.dp)) + .background(if (pressed) p.surface2 else Color.Transparent) + .border(1.dp, p.border2, RoundedCornerShape(6.dp)) + .clickable(interactionSource = src, indication = null, onClick = onClick), + contentAlignment = Alignment.Center, + ) { + DText(text, 15.sp, p.text1, weight = FontWeight.Medium) + } +} + +/** 卡片:surface-card 底 + border-1 + 圆角 14 + 内边距 16。 */ +@Composable +fun DCard(modifier: Modifier = Modifier, content: @Composable ColumnScope.() -> Unit) { + val p = LocalDuduPalette.current + Column( + modifier + .fillMaxWidth() + .clip(RoundedCornerShape(14.dp)) + .background(p.surfaceCard) + .border(1.dp, p.border1, RoundedCornerShape(14.dp)) + .padding(16.dp), + content = content, + ) +} + +/** 列表行入口(标题 + 右箭头),danger 行用于退出登录。 */ +@Composable +fun ListRow(title: String, danger: Boolean = false, onClick: () -> Unit) { + val p = LocalDuduPalette.current + val src = remember { MutableInteractionSource() } + Row( + Modifier + .fillMaxWidth() + .clickable(interactionSource = src, indication = null, onClick = onClick) + .padding(vertical = 14.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + DText(title, 15.sp, if (danger) p.danger else p.text1) + Spacer(Modifier.weight(1f)) + if (!danger) IconChevronRight(16.dp, p.text3) + } +} + +/** 顶栏:返回 + 标题(页面无二级导航时 onBack 为 null)。 */ +@Composable +fun TopBar(title: String, onBack: (() -> Unit)?) { + val p = LocalDuduPalette.current + Row( + Modifier + .fillMaxWidth() + .height(52.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + if (onBack != null) { + val src = remember { MutableInteractionSource() } + Box( + Modifier + .size(40.dp) + .clip(CircleShape) + .clickable(interactionSource = src, indication = null, onClick = onBack), + contentAlignment = Alignment.Center, + ) { + IconChevronLeft(20.dp, p.text1) + } + Spacer(Modifier.width(4.dp)) + } + DText(title, 17.sp, p.text1, weight = FontWeight.SemiBold) + } +} + +/** 浅蓝提示条(场景上下文)。 */ +@Composable +fun ContextBanner(text: String) { + val p = LocalDuduPalette.current + Box( + Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(10.dp)) + .background(p.accentSoft) + .padding(horizontal = 14.dp, vertical = 10.dp), + ) { + DText(text, 13.sp, p.accentText) + } +} + +/** 进度条(试用用量等)。 */ +@Composable +fun DProgressBar(fraction: Float, modifier: Modifier = Modifier) { + val p = LocalDuduPalette.current + Box( + modifier + .fillMaxWidth() + .height(6.dp) + .clip(RoundedCornerShape(999.dp)) + .background(p.surface2), + ) { + Box( + Modifier + .fillMaxWidth(fraction.coerceIn(0f, 1f)) + .fillMaxHeight() + .clip(RoundedCornerShape(999.dp)) + .background(p.accent), + ) + } +} + +/** 状态徽标:positive(余额充足)/ warning(余额不足)/ neutral。 */ +enum class BadgeKind { POSITIVE, WARNING, NEUTRAL } + +@Composable +fun Badge(text: String, kind: BadgeKind) { + val p = LocalDuduPalette.current + val (bg, fg) = when (kind) { + BadgeKind.POSITIVE -> p.positiveSoft to p.positive + BadgeKind.WARNING -> p.warningSoft to p.warning + BadgeKind.NEUTRAL -> p.surface2 to p.text2 + } + Box( + Modifier + .clip(RoundedCornerShape(999.dp)) + .background(bg) + .padding(horizontal = 10.dp, vertical = 3.dp), + ) { + DText(text, 12.sp, fg, weight = FontWeight.Medium) + } +} + +/** 开关(SettingRow 用,36x22 胶囊 + 圆点)。 */ +@Composable +fun DSwitch(checked: Boolean, onToggle: () -> Unit) { + val p = LocalDuduPalette.current + val src = remember { MutableInteractionSource() } + Box( + Modifier + .width(40.dp) + .height(24.dp) + .clip(RoundedCornerShape(999.dp)) + .background(if (checked) p.accent else p.border2) + .clickable(interactionSource = src, indication = null, onClick = onToggle) + .padding(3.dp), + contentAlignment = if (checked) Alignment.CenterEnd else Alignment.CenterStart, + ) { + Box( + Modifier + .size(18.dp) + .clip(CircleShape) + .background(p.textOnAccent), + ) + } +} + +/** 成功态绿对勾圆(56dp,购买完成 / 反馈成功共用)。 */ +@Composable +fun SuccessCheckCircle() { + val p = LocalDuduPalette.current + Box( + Modifier + .size(56.dp) + .clip(CircleShape) + .background(p.positiveSoft), + contentAlignment = Alignment.Center, + ) { + IconCheck(26.dp, p.positive) + } +} + +/** mock 四态切换 chip(离线联调,仅 debug 区显示)。 */ +@Composable +fun StateChip(label: String, selected: Boolean, onClick: () -> Unit) { + val p = LocalDuduPalette.current + val src = remember { MutableInteractionSource() } + Box( + Modifier + .clip(RoundedCornerShape(999.dp)) + .background(if (selected) p.accentSoft else p.surface2) + .border(1.dp, if (selected) p.accent else p.border1, RoundedCornerShape(999.dp)) + .clickable(interactionSource = src, indication = null, onClick = onClick) + .padding(horizontal = 14.dp, vertical = 7.dp), + ) { + DText(label, 13.sp, if (selected) p.accentText else p.text2) + } +} + +/** 错误/提示文案行(danger 色,表单下方)。 */ +@Composable +fun NoticeText(text: String?, danger: Boolean = true) { + val p = LocalDuduPalette.current + if (text != null) { + Spacer(Modifier.height(10.dp)) + DText(text, 13.sp, if (danger) p.danger else p.text2) + } +} + +/** 行间距快捷。 */ +@Composable +fun VSpace(dp: Int) = Spacer(Modifier.height(dp.dp)) + +/** 横向间距快捷。 */ +@Composable +fun HSpace(dp: Int) = Spacer(Modifier.width(dp.dp)) + +/** 分隔线。 */ +@Composable +fun Divider1() { + val p = LocalDuduPalette.current + Box( + Modifier + .fillMaxWidth() + .height(1.dp) + .background(p.border1), + ) +} + +/** 行排列快捷(垂直居中 + 8dp 间距)。 */ +@Composable +fun CenterRow(modifier: Modifier = Modifier, content: @Composable () -> Unit) { + Row( + modifier, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { content() } +} diff --git a/android/app/src/main/java/app/dudu/audio/AudioStreamer.kt b/android/app/src/main/java/app/dudu/audio/AudioStreamer.kt new file mode 100644 index 0000000..b53a724 --- /dev/null +++ b/android/app/src/main/java/app/dudu/audio/AudioStreamer.kt @@ -0,0 +1,76 @@ +package app.dudu.audio + +import android.annotation.SuppressLint +import android.media.AudioFormat +import android.media.AudioRecord +import android.media.MediaRecorder +import android.util.Log +import java.util.concurrent.atomic.AtomicBoolean +import kotlin.concurrent.thread + +/** + * 麦克风采集:16kHz / 16bit / mono PCM,按协议每 100ms 一帧 = 3200 字节 + * (对齐 server/pkg/protocol/ws.go: SampleRate/FrameMillis/FrameBytes)。 + * + * 调用方必须先确认 RECORD_AUDIO 已授权(IME 内无法弹权限框,未授权时引导主 App)。 + */ +class AudioStreamer( + private val onFrame: (ByteArray) -> Unit, + private val onError: (Throwable) -> Unit, +) { + private val running = AtomicBoolean(false) + private var worker: Thread? = null + + @SuppressLint("MissingPermission") // 权限由调用方(KeyboardController)预检 + fun start() { + if (!running.compareAndSet(false, true)) return + worker = thread(name = "dudu-audio") { + var record: AudioRecord? = null + try { + val minBuf = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL, ENCODING) + record = AudioRecord( + MediaRecorder.AudioSource.VOICE_RECOGNITION, + SAMPLE_RATE, CHANNEL, ENCODING, + maxOf(minBuf, FRAME_BYTES * 4), + ) + check(record.state == AudioRecord.STATE_INITIALIZED) { "AudioRecord 初始化失败" } + record.startRecording() + check(record.recordingState == AudioRecord.RECORDSTATE_RECORDING) { "麦克风被占用或不可用" } + + while (running.get()) { + val frame = ByteArray(FRAME_BYTES) + var off = 0 + while (off < FRAME_BYTES && running.get()) { + val n = record.read(frame, off, FRAME_BYTES - off) + if (n < 0) throw IllegalStateException("AudioRecord.read 错误码 $n") + off += n + } + if (off == FRAME_BYTES) onFrame(frame) + } + } catch (t: Throwable) { + if (running.get()) { + Log.w(TAG, "音频采集异常", t) + onError(t) + } + } finally { + runCatching { + record?.stop() + record?.release() + } + } + } + } + + fun stop() { + running.set(false) + worker = null + } + + companion object { + private const val TAG = "DuduAudio" + const val SAMPLE_RATE = 16000 + const val FRAME_BYTES = 3200 // 100ms * 16000Hz * 16bit mono + private const val CHANNEL = AudioFormat.CHANNEL_IN_MONO + private const val ENCODING = AudioFormat.ENCODING_PCM_16BIT + } +} diff --git a/android/app/src/main/java/app/dudu/data/AccountRepository.kt b/android/app/src/main/java/app/dudu/data/AccountRepository.kt new file mode 100644 index 0000000..760a7c4 --- /dev/null +++ b/android/app/src/main/java/app/dudu/data/AccountRepository.kt @@ -0,0 +1,207 @@ +package app.dudu.data + +import android.content.Context +import android.content.SharedPreferences +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import java.util.UUID + +/** 账户状态,对齐 server/pkg/protocol/dto.go 的 account_state + 客户端本地的"未登录"。 */ +enum class AccountState(val wire: String) { + OK("ok"), // 余额 > 0 + TRIAL("trial"), // 余额 = 0 且当日试用未尽 + GUEST("guest"), // 未登录(仅客户端态,服务端无此值) + QUOTA("quota"); // 试用 + 余额均尽 + + companion object { + fun fromWire(s: String?): AccountState = + entries.firstOrNull { it.wire == s } ?: GUEST + } +} + +data class AccountSnapshot( + val state: AccountState, + val balanceSeconds: Long, + val trialRemainingSeconds: Int, + /** 每日试用上限(秒,/v1/me trial_daily_limit),默认 180 = 3 分钟 */ + val trialDailyLimitSeconds: Int, + /** 今日已用试用(秒,/v1/me trial_used_today) */ + val trialUsedTodaySeconds: Int, + val token: String?, // JWT,未登录为 null + val nicknameMasked: String?, + val userId: String?, +) + +/** + * 账户仓库:状态持久化在 SharedPreferences,主 App 与 IME Service 同进程共享。 + * + * 真实路径(13B 起):微信登录 [setLogin] 写入 JWT → GET /v1/me 经 [applyMe] 刷新; + * WS usage 帧经 [applyUsage] 实时刷新余额/试用,与 /v1/me 互为补充。 + * mock 路径(保留做离线联调):[setMockState] 手动切四态,仅 debug 入口可见。 + * + * TODO(secure-store): JWT 现存明文 SharedPreferences,上线前迁 EncryptedSharedPreferences / Keystore。 + */ +class AccountRepository private constructor(context: Context) { + + private val prefs: SharedPreferences = + context.applicationContext.getSharedPreferences(PREFS, Context.MODE_PRIVATE) + + private val _snapshot = MutableStateFlow(load()) + val snapshot: StateFlow = _snapshot + + // 监听跨组件(主 App ↔ IME)的写入,保持两边实时一致 + private val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, _ -> + _snapshot.value = load() + } + + init { + prefs.registerOnSharedPreferenceChangeListener(listener) + } + + /** 设备 ID:首次生成 UUID 并持久化,用于 WS X-Device-ID 头。 */ + val deviceId: String + get() { + prefs.getString(KEY_DEVICE_ID, null)?.let { return it } + val id = "and-" + UUID.randomUUID().toString() + prefs.edit().putString(KEY_DEVICE_ID, id).apply() + return id + } + + private fun load(): AccountSnapshot { + val state = AccountState.fromWire(prefs.getString(KEY_STATE, AccountState.GUEST.wire)) + return AccountSnapshot( + state = state, + balanceSeconds = prefs.getLong(KEY_BALANCE, 0L), + trialRemainingSeconds = prefs.getInt(KEY_TRIAL_REMAIN, 0), + trialDailyLimitSeconds = prefs.getInt(KEY_TRIAL_LIMIT, DEFAULT_TRIAL_LIMIT), + trialUsedTodaySeconds = prefs.getInt(KEY_TRIAL_USED, 0), + token = prefs.getString(KEY_TOKEN, null), + nicknameMasked = prefs.getString(KEY_NICKNAME, null), + userId = prefs.getString(KEY_USER_ID, null), + ) + } + + // ─── 真实登录路径(13B) ───────────────────────────────────────────────── + + /** 微信登录成功(POST /v1/auth/wechat):写入 JWT 与用户信息,随后应调 /v1/me 校正余额。 */ + fun setLogin(token: String, userId: String, nicknameMasked: String) { + prefs.edit() + .putString(KEY_TOKEN, token) + .putString(KEY_USER_ID, userId) + .putString(KEY_NICKNAME, nicknameMasked) + .putString(KEY_STATE, AccountState.TRIAL.wire) // 占位,applyMe 立刻校正 + .apply() + } + + /** GET /v1/me → 刷新余额/试用/账户态(字段对齐 protocol.MeResponse,试用单位为秒)。 */ + fun applyMe( + userId: String, + nicknameMasked: String, + balanceSeconds: Long, + trialDailyLimit: Int, + trialUsedToday: Int, + accountState: String, + ) { + prefs.edit() + .putString(KEY_USER_ID, userId) + .putString(KEY_NICKNAME, nicknameMasked) + .putLong(KEY_BALANCE, balanceSeconds) + .putInt(KEY_TRIAL_LIMIT, trialDailyLimit) + .putInt(KEY_TRIAL_USED, trialUsedToday) + .putInt(KEY_TRIAL_REMAIN, (trialDailyLimit - trialUsedToday).coerceAtLeast(0)) + .putString(KEY_STATE, accountState) + .apply() + } + + /** 退出登录:清 JWT 与用户信息,回游客态(服务端 /v1/auth/logout 由调用方发起)。 */ + fun logout() { + prefs.edit() + .remove(KEY_TOKEN) + .remove(KEY_USER_ID) + .remove(KEY_NICKNAME) + .putLong(KEY_BALANCE, 0L) + .putString(KEY_STATE, AccountState.GUEST.wire) + .apply() + } + + /** WS usage 帧 → 实时刷新余额与试用剩余。 */ + fun applyUsage(balanceSeconds: Long, trialRemaining: Int) { + val limit = prefs.getInt(KEY_TRIAL_LIMIT, DEFAULT_TRIAL_LIMIT) + prefs.edit() + .putLong(KEY_BALANCE, balanceSeconds) + .putInt(KEY_TRIAL_REMAIN, trialRemaining) + .putInt(KEY_TRIAL_USED, (limit - trialRemaining).coerceAtLeast(0)) + .apply() + // 余额/试用归零时联动账户态(与 dto.go 判定规则一致) + val cur = _snapshot.value.state + if (cur == AccountState.OK || cur == AccountState.TRIAL) { + val next = when { + balanceSeconds > 0 -> AccountState.OK + trialRemaining > 0 -> AccountState.TRIAL + else -> AccountState.QUOTA + } + if (next != cur) prefs.edit().putString(KEY_STATE, next.wire).apply() + } + } + + /** 当前是否处于离线 mock 会话(setMockState 写入的假 token,不可对真实后端发请求)。 */ + val offlineMock: Boolean + get() = prefs.getString(KEY_TOKEN, null) == MOCK_TOKEN + + /** 服务端回 QUOTA_EXCEEDED → 本地立刻转 quota 态。 */ + fun markQuotaExceeded() { + prefs.edit().putString(KEY_STATE, AccountState.QUOTA.wire).apply() + } + + /** 服务端回 UNAUTHORIZED → 清登录态。 */ + fun markUnauthorized() { + prefs.edit() + .remove(KEY_TOKEN) + .putString(KEY_STATE, AccountState.GUEST.wire) + .apply() + } + + // ─── 离线 mock 开关:无后端时手动切四态做联调(仅 debug UI 暴露入口) ──── + // 真实登录路径见 setLogin/applyMe;本方法保留用于后端不可用时的键盘联调 + fun setMockState(state: AccountState) { + val e = prefs.edit().putString(KEY_STATE, state.wire).putInt(KEY_TRIAL_LIMIT, DEFAULT_TRIAL_LIMIT) + when (state) { + AccountState.OK -> e.putLong(KEY_BALANCE, 472L * 60).putInt(KEY_TRIAL_REMAIN, 120).putInt(KEY_TRIAL_USED, 60) + .putString(KEY_TOKEN, MOCK_TOKEN).putString(KEY_NICKNAME, "wang***") + AccountState.TRIAL -> e.putLong(KEY_BALANCE, 0L).putInt(KEY_TRIAL_REMAIN, 120).putInt(KEY_TRIAL_USED, 60) + .putString(KEY_TOKEN, MOCK_TOKEN).putString(KEY_NICKNAME, "wang***") + AccountState.QUOTA -> e.putLong(KEY_BALANCE, 0L).putInt(KEY_TRIAL_REMAIN, 0).putInt(KEY_TRIAL_USED, DEFAULT_TRIAL_LIMIT) + .putString(KEY_TOKEN, MOCK_TOKEN).putString(KEY_NICKNAME, "wang***") + AccountState.GUEST -> e.putLong(KEY_BALANCE, 0L).putInt(KEY_TRIAL_REMAIN, 0).putInt(KEY_TRIAL_USED, 0) + .remove(KEY_TOKEN).remove(KEY_NICKNAME) + } + e.apply() + } + + companion object { + private const val PREFS = "dudu_account" + private const val KEY_STATE = "state" + private const val KEY_BALANCE = "balance_seconds" + private const val KEY_TRIAL_REMAIN = "trial_remaining_seconds" + private const val KEY_TRIAL_LIMIT = "trial_daily_limit_seconds" + private const val KEY_TRIAL_USED = "trial_used_today_seconds" + private const val KEY_TOKEN = "token" + private const val KEY_NICKNAME = "nickname_masked" + private const val KEY_USER_ID = "user_id" + private const val KEY_DEVICE_ID = "device_id" + + /** 每日试用 3 分钟(与 server protocol.TrialDailySeconds 一致,/v1/me 会校正)。 */ + private const val DEFAULT_TRIAL_LIMIT = 180 + + // 离线 mock 开关用的假 JWT(dev server 不校验时可直接联调键盘 UI) + private const val MOCK_TOKEN = "dev-mock-token" + + @Volatile + private var instance: AccountRepository? = null + + fun get(context: Context): AccountRepository = + instance ?: synchronized(this) { + instance ?: AccountRepository(context).also { instance = it } + } + } +} diff --git a/android/app/src/main/java/app/dudu/data/MetricsFlushWorker.kt b/android/app/src/main/java/app/dudu/data/MetricsFlushWorker.kt new file mode 100644 index 0000000..10845f2 --- /dev/null +++ b/android/app/src/main/java/app/dudu/data/MetricsFlushWorker.kt @@ -0,0 +1,61 @@ +package app.dudu.data + +import android.content.Context +import androidx.work.BackoffPolicy +import androidx.work.Constraints +import androidx.work.CoroutineWorker +import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.ExistingWorkPolicy +import androidx.work.NetworkType +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.PeriodicWorkRequestBuilder +import androidx.work.WorkManager +import androidx.work.WorkerParameters +import java.util.concurrent.TimeUnit + +/** + * 埋点上报 Worker:把 MetricsQueue 批量 flush 到 POST /v1/metrics/batch。 + * 失败 → Result.retry(),按指数退避(30s 起),最多 5 次后放弃(下个周期再试)。 + */ +class MetricsFlushWorker(appContext: Context, params: WorkerParameters) : + CoroutineWorker(appContext, params) { + + override suspend fun doWork(): Result { + val ok = MetricsQueue.get(applicationContext).flush() + return when { + ok -> Result.success() + runAttemptCount < MAX_ATTEMPTS -> Result.retry() + else -> Result.failure() // 事件仍留在队列里,下个周期继续 + } + } + + companion object { + private const val MAX_ATTEMPTS = 5 + private const val PERIODIC_WORK = "dudu-metrics-flush" + private const val STARTUP_WORK = "dudu-metrics-flush-now" + + /** App 启动时调用:注册周期 flush(15 分钟,WorkManager 周期下限)+ 冷启动立即 flush 一次。 */ + fun schedule(context: Context) { + val constraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() + val wm = WorkManager.getInstance(context) + wm.enqueueUniquePeriodicWork( + PERIODIC_WORK, + ExistingPeriodicWorkPolicy.KEEP, + PeriodicWorkRequestBuilder(15, TimeUnit.MINUTES) + .setConstraints(constraints) + .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 30, TimeUnit.SECONDS) + .build(), + ) + wm.enqueueUniqueWork( + STARTUP_WORK, + ExistingWorkPolicy.KEEP, + OneTimeWorkRequestBuilder() + .setConstraints(constraints) + .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 30, TimeUnit.SECONDS) + .build(), + ) + } + } +} diff --git a/android/app/src/main/java/app/dudu/data/MetricsQueue.kt b/android/app/src/main/java/app/dudu/data/MetricsQueue.kt new file mode 100644 index 0000000..37a06b2 --- /dev/null +++ b/android/app/src/main/java/app/dudu/data/MetricsQueue.kt @@ -0,0 +1,133 @@ +package app.dudu.data + +import android.content.Context +import android.os.Build +import app.dudu.BuildConfig +import app.dudu.net.DuduApi +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch +import org.json.JSONArray +import org.json.JSONObject +import java.io.File + +/** + * 埋点队列:本地 JSONL 文件(设计文档 9.2:满批/定时批量上报,失败不影响功能)。 + * IME Service 与主 App 同进程共用此单例(设计文档 9.3 Android 行), + * flush 由 WorkManager 周期触发(见 MetricsFlushWorker)+ 冷启动一次。 + * + * 隐私底线:事件 props 不携带识别文本与音频。 + */ +class MetricsQueue private constructor(private val context: Context) { + + private val file = File(context.filesDir, "metrics_queue.jsonl") + private val prefs = context.getSharedPreferences("dudu_metrics", Context.MODE_PRIVATE) + private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + private val lock = Any() + + /** 记一条事件(异步落盘,任意线程可调)。未在白名单的事件名直接丢弃。 */ + fun track(event: String, props: Map = emptyMap()) { + if (event !in WHITELIST) return + val line = JSONObject() + .put("event", event) + .put("client_ts", System.currentTimeMillis()) + .apply { if (props.isNotEmpty()) put("props", JSONObject(props)) } + .toString() + ioScope.launch { synchronized(lock) { append(line) } } + } + + /** 低频漏斗事件去重:同 key 只记一次(如 kb.enabled)。 */ + fun trackOnce(key: String, event: String, props: Map = emptyMap()) { + if (prefs.getBoolean("once_$key", false)) return + prefs.edit().putBoolean("once_$key", true).apply() + track(event, props) + } + + /** + * 把队列分批(≤100 条/批,对齐 protocol.MetricsMaxBatch)POST /v1/metrics/batch(gzip)。 + * @return 全部发送成功(队列空也算成功);失败由调用方(Worker)退避重试 + */ + suspend fun flush(): Boolean { + val pending = synchronized(lock) { + if (!file.exists()) emptyList() else file.readLines().filter { it.isNotBlank() } + } + if (pending.isEmpty()) return true + + val account = AccountRepository.get(context) + var sent = 0 + for (chunk in pending.chunked(MAX_BATCH)) { + val events = JSONArray() + chunk.forEach { line -> runCatching { events.put(JSONObject(line)) } } + val body = JSONObject() + .put("device_id", account.deviceId) + .put("platform", "android") + .put("app_version", BuildConfig.VERSION_NAME) + .put("os_version", "Android " + Build.VERSION.RELEASE) + .put("events", events) + .toString() + if (!DuduApi.metricsBatch(body)) break + sent += chunk.size + } + if (sent > 0) { + synchronized(lock) { + // flush 期间可能有新事件追加:只删掉已发送的前 sent 行 + val cur = if (file.exists()) file.readLines().filter { it.isNotBlank() } else emptyList() + val rest = if (sent >= cur.size) emptyList() else cur.drop(sent) + rewrite(rest) + } + } + return sent == pending.size + } + + // ─── 内部 ──────────────────────────────────────────────────────────────── + + @Volatile + private var approxCount = -1 + + private fun append(line: String) { + if (approxCount < 0) { + approxCount = if (file.exists()) file.readLines().count { it.isNotBlank() } else 0 + } + file.appendText(line + "\n") + approxCount++ + // 队列上限 1000 条,超出丢最旧(设计文档 9.2) + if (approxCount > MAX_QUEUE) { + rewrite(file.readLines().filter { it.isNotBlank() }.takeLast(MAX_QUEUE)) + } + } + + private fun rewrite(lines: List) { + if (lines.isEmpty()) file.delete() + else file.writeText(lines.joinToString("\n") + "\n") + approxCount = lines.size + } + + companion object { + private const val MAX_QUEUE = 1000 + private const val MAX_BATCH = 100 // protocol.MetricsMaxBatch + + /** 事件白名单——与 server/pkg/protocol/dto.go 的 MetricWhitelist 保持同步。 */ + private val WHITELIST = setOf( + "asr.first_partial_ms", + "asr.release_to_commit_ms", + "audio.start_ms", + "ws.connect_ms", + "ws.reconnect", + "asr.session", + "inject.fail", + "onboarding.step", + "login.success", + "purchase.success", + "kb.enabled", + ) + + @Volatile + private var instance: MetricsQueue? = null + + fun get(context: Context): MetricsQueue = + instance ?: synchronized(this) { + instance ?: MetricsQueue(context.applicationContext).also { instance = it } + } + } +} diff --git a/android/app/src/main/java/app/dudu/design/DText.kt b/android/app/src/main/java/app/dudu/design/DText.kt new file mode 100644 index 0000000..e9395d7 --- /dev/null +++ b/android/app/src/main/java/app/dudu/design/DText.kt @@ -0,0 +1,41 @@ +package app.dudu.design + +import androidx.compose.foundation.text.BasicText +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.sp + +/** 统一文本组件:系统字体(中文走 PingFang/MiSans 等),mono 仅用于数字/计时。 */ +@Composable +fun DText( + text: String, + size: TextUnit, + color: Color, + modifier: Modifier = Modifier, + weight: FontWeight = FontWeight.Normal, + mono: Boolean = false, + lineHeight: TextUnit = TextUnit.Unspecified, + textAlign: TextAlign? = null, + maxLines: Int = Int.MAX_VALUE, +) { + BasicText( + text = text, + modifier = modifier, + maxLines = maxLines, + style = TextStyle( + color = color, + fontSize = size, + fontWeight = weight, + fontFamily = if (mono) FontFamily.Monospace else FontFamily.Default, + lineHeight = lineHeight, + textAlign = textAlign ?: TextAlign.Unspecified, + letterSpacing = 0.sp, + ), + ) +} diff --git a/android/app/src/main/java/app/dudu/design/DuduIcons.kt b/android/app/src/main/java/app/dudu/design/DuduIcons.kt new file mode 100644 index 0000000..64ff08d --- /dev/null +++ b/android/app/src/main/java/app/dudu/design/DuduIcons.kt @@ -0,0 +1,255 @@ +package app.dudu.design + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.StrokeJoin +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +/** + * 线条图标(对齐 Lucide 风格:2px 圆头描边、24 网格比例),用 Canvas 手绘, + * 避免引入图标库依赖;颜色跟随传入 color(等价 currentColor)。 + * 设计规则:不用 emoji、不用 unicode 符号作图标。 + */ + +private fun DrawScope.lineStroke(w: Float): Stroke = + Stroke(width = w, cap = StrokeCap.Round, join = StrokeJoin.Round) + +private fun DrawScope.strokeW(side: Float): Float = side * 2f / 24f + +/** dudu logo mark:圆环(嘟起的嘴)+ 三条声波竖线。 */ +@Composable +fun DuduLogoMark(size: Dp, color: Color, modifier: Modifier = Modifier) { + Canvas(modifier.size(size)) { + val s = this.size.minDimension + val w = s * 3.5f / 24f // logo 用 3.5px/24 笔画 + val stroke = lineStroke(w) + drawCircle(color = color, radius = s * 0.42f, center = center, style = stroke) + val bar = { x: Float, h: Float -> + drawLine(color, Offset(s * x, center.y - h / 2), Offset(s * x, center.y + h / 2), w, StrokeCap.Round) + } + bar(0.36f, s * 0.18f) + bar(0.50f, s * 0.34f) + bar(0.64f, s * 0.18f) + } +} + +/** 麦克风。 */ +@Composable +fun IconMic(size: Dp, color: Color, modifier: Modifier = Modifier) { + Canvas(modifier.size(size)) { + val s = this.size.minDimension + val w = strokeW(s) + val stroke = lineStroke(w) + // 话筒身:圆角胶囊 + val bodyW = s * 0.25f + drawRoundRect( + color = color, + topLeft = Offset(center.x - bodyW / 2, s * 0.08f), + size = Size(bodyW, s * 0.5f), + cornerRadius = androidx.compose.ui.geometry.CornerRadius(bodyW / 2), + style = stroke, + ) + // 拾音弧 + val arcR = s * 0.29f + drawArc( + color = color, + startAngle = 0f, + sweepAngle = 180f, + useCenter = false, + topLeft = Offset(center.x - arcR, s * 0.42f - arcR), + size = Size(arcR * 2, arcR * 2), + style = stroke, + ) + // 支杆 + drawLine(color, Offset(center.x, s * 0.71f), Offset(center.x, s * 0.92f), w, StrokeCap.Round) + } +} + +/** 对勾。 */ +@Composable +fun IconCheck(size: Dp, color: Color, modifier: Modifier = Modifier) { + Canvas(modifier.size(size)) { + val s = this.size.minDimension + val w = strokeW(s) + val p = Path().apply { + moveTo(s * 0.17f, s * 0.55f) + lineTo(s * 0.40f, s * 0.78f) + lineTo(s * 0.83f, s * 0.25f) + } + drawPath(p, color, style = lineStroke(w)) + } +} + +/** 上箭头。 */ +@Composable +fun IconArrowUp(size: Dp, color: Color, modifier: Modifier = Modifier) { + Canvas(modifier.size(size)) { + val s = this.size.minDimension + val w = strokeW(s) + drawLine(color, Offset(center.x, s * 0.17f), Offset(center.x, s * 0.83f), w, StrokeCap.Round) + val p = Path().apply { + moveTo(s * 0.25f, s * 0.42f) + lineTo(center.x, s * 0.17f) + lineTo(s * 0.75f, s * 0.42f) + } + drawPath(p, color, style = lineStroke(w)) + } +} + +/** 地球(切换输入法)。 */ +@Composable +fun IconGlobe(size: Dp, color: Color, modifier: Modifier = Modifier) { + Canvas(modifier.size(size)) { + val s = this.size.minDimension + val w = strokeW(s) + val stroke = lineStroke(w) + drawCircle(color, radius = s * 0.4f, center = center, style = stroke) + drawLine(color, Offset(s * 0.1f, center.y), Offset(s * 0.9f, center.y), w, StrokeCap.Round) + drawOval( + color = color, + topLeft = Offset(center.x - s * 0.18f, s * 0.1f), + size = Size(s * 0.36f, s * 0.8f), + style = stroke, + ) + } +} + +/** 退格。 */ +@Composable +fun IconBackspace(size: Dp, color: Color, modifier: Modifier = Modifier) { + Canvas(modifier.size(size)) { + val s = this.size.minDimension + val w = strokeW(s) + val stroke = lineStroke(w) + val body = Path().apply { + moveTo(s * 0.36f, s * 0.2f) + lineTo(s * 0.88f, s * 0.2f) + lineTo(s * 0.88f, s * 0.8f) + lineTo(s * 0.36f, s * 0.8f) + lineTo(s * 0.08f, s * 0.5f) + close() + } + drawPath(body, color, style = stroke) + drawLine(color, Offset(s * 0.5f, s * 0.38f), Offset(s * 0.72f, s * 0.62f), w, StrokeCap.Round) + drawLine(color, Offset(s * 0.72f, s * 0.38f), Offset(s * 0.5f, s * 0.62f), w, StrokeCap.Round) + } +} + +/** 回车(corner-down-left)。 */ +@Composable +fun IconEnter(size: Dp, color: Color, modifier: Modifier = Modifier) { + Canvas(modifier.size(size)) { + val s = this.size.minDimension + val w = strokeW(s) + val stroke = lineStroke(w) + val p = Path().apply { + moveTo(s * 0.83f, s * 0.21f) + lineTo(s * 0.83f, s * 0.46f) + quadraticTo(s * 0.83f, s * 0.62f, s * 0.67f, s * 0.62f) + lineTo(s * 0.17f, s * 0.62f) + } + drawPath(p, color, style = stroke) + val head = Path().apply { + moveTo(s * 0.38f, s * 0.42f) + lineTo(s * 0.17f, s * 0.62f) + lineTo(s * 0.38f, s * 0.83f) + } + drawPath(head, color, style = stroke) + } +} + +/** 设置(sliders-horizontal)。 */ +@Composable +fun IconSliders(size: Dp, color: Color, modifier: Modifier = Modifier) { + Canvas(modifier.size(size)) { + val s = this.size.minDimension + val w = strokeW(s) + val stroke = lineStroke(w) + drawLine(color, Offset(s * 0.1f, s * 0.33f), Offset(s * 0.9f, s * 0.33f), w, StrokeCap.Round) + drawLine(color, Offset(s * 0.1f, s * 0.67f), Offset(s * 0.9f, s * 0.67f), w, StrokeCap.Round) + drawCircle(color, radius = s * 0.1f, center = Offset(s * 0.62f, s * 0.33f), style = stroke) + drawCircle(color, radius = s * 0.1f, center = Offset(s * 0.38f, s * 0.67f), style = stroke) + } +} + +/** 右箭头(列表行入口)。 */ +@Composable +fun IconChevronRight(size: Dp, color: Color, modifier: Modifier = Modifier) { + Canvas(modifier.size(size)) { + val s = this.size.minDimension + val w = strokeW(s) + val p = Path().apply { + moveTo(s * 0.38f, s * 0.2f) + lineTo(s * 0.66f, s * 0.5f) + lineTo(s * 0.38f, s * 0.8f) + } + drawPath(p, color, style = lineStroke(w)) + } +} + +/** 左箭头(返回)。 */ +@Composable +fun IconChevronLeft(size: Dp, color: Color, modifier: Modifier = Modifier) { + Canvas(modifier.size(size)) { + val s = this.size.minDimension + val w = strokeW(s) + val p = Path().apply { + moveTo(s * 0.62f, s * 0.2f) + lineTo(s * 0.34f, s * 0.5f) + lineTo(s * 0.62f, s * 0.8f) + } + drawPath(p, color, style = lineStroke(w)) + } +} + +/** 加号(image-plus 的简化形:添加图片框内用)。 */ +@Composable +fun IconPlus(size: Dp, color: Color, modifier: Modifier = Modifier) { + Canvas(modifier.size(size)) { + val s = this.size.minDimension + val w = strokeW(s) + drawLine(color, Offset(center.x, s * 0.2f), Offset(center.x, s * 0.8f), w, StrokeCap.Round) + drawLine(color, Offset(s * 0.2f, center.y), Offset(s * 0.8f, center.y), w, StrokeCap.Round) + } +} + +/** 叉(删除图片)。 */ +@Composable +fun IconX(size: Dp, color: Color, modifier: Modifier = Modifier) { + Canvas(modifier.size(size)) { + val s = this.size.minDimension + val w = strokeW(s) + drawLine(color, Offset(s * 0.25f, s * 0.25f), Offset(s * 0.75f, s * 0.75f), w, StrokeCap.Round) + drawLine(color, Offset(s * 0.75f, s * 0.25f), Offset(s * 0.25f, s * 0.75f), w, StrokeCap.Round) + } +} + +/** 用户(账号卡头像占位)。 */ +@Composable +fun IconUser(size: Dp, color: Color, modifier: Modifier = Modifier) { + Canvas(modifier.size(size)) { + val s = this.size.minDimension + val w = strokeW(s) + val stroke = lineStroke(w) + drawCircle(color, radius = s * 0.18f, center = Offset(center.x, s * 0.32f), style = stroke) + drawArc( + color = color, + startAngle = 180f, + sweepAngle = 180f, + useCenter = false, + topLeft = Offset(s * 0.2f, s * 0.58f), + size = Size(s * 0.6f, s * 0.62f), + style = stroke, + ) + } +} diff --git a/android/app/src/main/java/app/dudu/design/DuduPalette.kt b/android/app/src/main/java/app/dudu/design/DuduPalette.kt new file mode 100644 index 0000000..734f14a --- /dev/null +++ b/android/app/src/main/java/app/dudu/design/DuduPalette.kt @@ -0,0 +1,106 @@ +package app.dudu.design + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.graphics.Color + +/** + * 语义化调色板:把自动生成的 DuduTheme.Light / DuduTheme.Dark(object,无公共类型) + * 收敛成一个可注入 CompositionLocal 的 data class。颜色值全部来自 DuduTheme,禁止自造。 + */ +@Immutable +data class DuduPalette( + val accent: Color, + val accentHover: Color, + val accentPress: Color, + val accentSoft: Color, + val accentText: Color, + val bgApp: Color, + val surfaceCard: Color, + val surface2: Color, + val surface3: Color, + val border1: Color, + val border2: Color, + val text1: Color, + val text2: Color, + val text3: Color, + val textOnAccent: Color, + val positive: Color, + val positiveSoft: Color, + val warning: Color, + val warningSoft: Color, + val danger: Color, + val dangerSoft: Color, + val overlayBg: Color, + val overlayText: Color, + val overlayText2: Color, + val overlayText3: Color, +) + +val DuduLightPalette = DuduPalette( + accent = DuduTheme.Light.accent, + accentHover = DuduTheme.Light.accentHover, + accentPress = DuduTheme.Light.accentPress, + accentSoft = DuduTheme.Light.accentSoft, + accentText = DuduTheme.Light.accentText, + bgApp = DuduTheme.Light.bgApp, + surfaceCard = DuduTheme.Light.surfaceCard, + surface2 = DuduTheme.Light.surface2, + surface3 = DuduTheme.Light.surface3, + border1 = DuduTheme.Light.border1, + border2 = DuduTheme.Light.border2, + text1 = DuduTheme.Light.text1, + text2 = DuduTheme.Light.text2, + text3 = DuduTheme.Light.text3, + textOnAccent = DuduTheme.Light.textOnAccent, + positive = DuduTheme.Light.positive, + positiveSoft = DuduTheme.Light.positiveSoft, + warning = DuduTheme.Light.warning, + warningSoft = DuduTheme.Light.warningSoft, + danger = DuduTheme.Light.danger, + dangerSoft = DuduTheme.Light.dangerSoft, + overlayBg = DuduTheme.Light.overlayBg, + overlayText = DuduTheme.Light.overlayText, + overlayText2 = DuduTheme.Light.overlayText2, + overlayText3 = DuduTheme.Light.overlayText3, +) + +val DuduDarkPalette = DuduPalette( + accent = DuduTheme.Dark.accent, + accentHover = DuduTheme.Dark.accentHover, + accentPress = DuduTheme.Dark.accentPress, + accentSoft = DuduTheme.Dark.accentSoft, + accentText = DuduTheme.Dark.accentText, + bgApp = DuduTheme.Dark.bgApp, + surfaceCard = DuduTheme.Dark.surfaceCard, + surface2 = DuduTheme.Dark.surface2, + surface3 = DuduTheme.Dark.surface3, + border1 = DuduTheme.Dark.border1, + border2 = DuduTheme.Dark.border2, + text1 = DuduTheme.Dark.text1, + text2 = DuduTheme.Dark.text2, + text3 = DuduTheme.Dark.text3, + textOnAccent = DuduTheme.Dark.textOnAccent, + positive = DuduTheme.Dark.positive, + positiveSoft = DuduTheme.Dark.positiveSoft, + warning = DuduTheme.Dark.warning, + warningSoft = DuduTheme.Dark.warningSoft, + danger = DuduTheme.Dark.danger, + dangerSoft = DuduTheme.Dark.dangerSoft, + overlayBg = DuduTheme.Dark.overlayBg, + overlayText = DuduTheme.Dark.overlayText, + overlayText2 = DuduTheme.Dark.overlayText2, + overlayText3 = DuduTheme.Dark.overlayText3, +) + +val LocalDuduPalette = staticCompositionLocalOf { DuduLightPalette } + +/** 主题入口:light/dark 跟随系统。 */ +@Composable +fun DuduDesign(content: @Composable () -> Unit) { + val palette = if (isSystemInDarkTheme()) DuduDarkPalette else DuduLightPalette + CompositionLocalProvider(LocalDuduPalette provides palette, content = content) +} diff --git a/android/app/src/main/java/app/dudu/design/DuduTheme.kt b/android/app/src/main/java/app/dudu/design/DuduTheme.kt new file mode 100644 index 0000000..0039d6d --- /dev/null +++ b/android/app/src/main/java/app/dudu/design/DuduTheme.kt @@ -0,0 +1,156 @@ +// 自动生成 — 请勿手改。来源 design/tokens/*.css +// 重新生成:node design-pipeline/export-tokens.mjs + +package app.dudu.design + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp + +object DuduTheme { + object Light { + val accent = Color(0xFF4F6EF7) + val accentHover = Color(0xFF3D58DB) + val accentPress = Color(0xFF2F44B3) + val accentSoft = Color(0xFFEEF1FE) + val accentSoft2 = Color(0xFFDFE5FD) + val accentText = Color(0xFF3D58DB) + val amber100 = Color(0xFFFCEFD7) + val amber500 = Color(0xFFE8890C) + val bgApp = Color(0xFFF6F7FA) + val blue100 = Color(0xFFDFE5FD) + val blue200 = Color(0xFFC4CFFB) + val blue300 = Color(0xFF9FB1F9) + val blue400 = Color(0xFF7590F8) + val blue50 = Color(0xFFEEF1FE) + val blue500 = Color(0xFF4F6EF7) + val blue600 = Color(0xFF3D58DB) + val blue700 = Color(0xFF2F44B3) + val blue800 = Color(0xFF25368C) + val blue900 = Color(0xFF1E2B6B) + val border1 = Color(0xFFE4E6EB) + val border2 = Color(0xFFD2D5DD) + val danger = Color(0xFFDC2626) + val dangerSoft = Color(0xFFFEE2E2) + val gray0 = Color(0xFFFFFFFF) + val gray100 = Color(0xFFF0F1F4) + val gray200 = Color(0xFFE4E6EB) + val gray25 = Color(0xFFFCFCFD) + val gray300 = Color(0xFFD2D5DD) + val gray400 = Color(0xFFA6ABB8) + val gray50 = Color(0xFFF6F7FA) + val gray500 = Color(0xFF7A8090) + val gray600 = Color(0xFF5A6072) + val gray700 = Color(0xFF434957) + val gray800 = Color(0xFF2B2F3A) + val gray900 = Color(0xFF1B1E26) + val gray950 = Color(0xFF11131A) + val green100 = Color(0xFFDCFCE7) + val green500 = Color(0xFF16A34A) + val overlayBg = Color(0xF2181A22) + val overlayText = Color(0xFFF2F4FA) + val overlayText2 = Color(0xFF9AA3BD) + val overlayText3 = Color(0xFF646E8C) + val positive = Color(0xFF16A34A) + val positiveSoft = Color(0xFFDCFCE7) + val red100 = Color(0xFFFEE2E2) + val red500 = Color(0xFFDC2626) + val surface2 = Color(0xFFF0F1F4) + val surface3 = Color(0xFFE4E6EB) + val surfaceCard = Color(0xFFFFFFFF) + val text1 = Color(0xFF1B1E26) + val text2 = Color(0xFF5A6072) + val text3 = Color(0xFFA6ABB8) + val textOnAccent = Color(0xFFFFFFFF) + val warning = Color(0xFFE8890C) + val warningSoft = Color(0xFFFCEFD7) + } + + object Dark { + val accent = Color(0xFF5C77E8) + val accentHover = Color(0xFF6E89F9) + val accentPress = Color(0xFF4A63D6) + val accentSoft = Color(0x295C77E8) + val accentSoft2 = Color(0x425C77E8) + val accentText = Color(0xFF7D95F7) + val amber100 = Color(0xFFFCEFD7) + val amber500 = Color(0xFFE8890C) + val bgApp = Color(0xFF0F1116) + val blue100 = Color(0xFFDFE5FD) + val blue200 = Color(0xFFC4CFFB) + val blue300 = Color(0xFF9FB1F9) + val blue400 = Color(0xFF7590F8) + val blue50 = Color(0xFFEEF1FE) + val blue500 = Color(0xFF4F6EF7) + val blue600 = Color(0xFF3D58DB) + val blue700 = Color(0xFF2F44B3) + val blue800 = Color(0xFF25368C) + val blue900 = Color(0xFF1E2B6B) + val border1 = Color(0xFF272C38) + val border2 = Color(0xFF343A49) + val danger = Color(0xFFF05B5B) + val dangerSoft = Color(0x29F05B5B) + val gray0 = Color(0xFFFFFFFF) + val gray100 = Color(0xFFF0F1F4) + val gray200 = Color(0xFFE4E6EB) + val gray25 = Color(0xFFFCFCFD) + val gray300 = Color(0xFFD2D5DD) + val gray400 = Color(0xFFA6ABB8) + val gray50 = Color(0xFFF6F7FA) + val gray500 = Color(0xFF7A8090) + val gray600 = Color(0xFF5A6072) + val gray700 = Color(0xFF434957) + val gray800 = Color(0xFF2B2F3A) + val gray900 = Color(0xFF1B1E26) + val gray950 = Color(0xFF11131A) + val green100 = Color(0xFFDCFCE7) + val green500 = Color(0xFF16A34A) + val overlayBg = Color(0xF51E212B) + val overlayText = Color(0xFFF2F4FA) + val overlayText2 = Color(0xFF9AA3BD) + val overlayText3 = Color(0xFF646E8C) + val positive = Color(0xFF34C46A) + val positiveSoft = Color(0x2934C46A) + val red100 = Color(0xFFFEE2E2) + val red500 = Color(0xFFDC2626) + val surface2 = Color(0xFF1E222C) + val surface3 = Color(0xFF262B37) + val surfaceCard = Color(0xFF171A22) + val text1 = Color(0xFFECEEF4) + val text2 = Color(0xFF9AA1B2) + val text3 = Color(0xFF5F6678) + val textOnAccent = Color(0xFFFFFFFF) + val warning = Color(0xFFF2A33C) + val warningSoft = Color(0x29F2A33C) + } + + // Dimensions (dp) + val controlLg = 44.dp + val controlMd = 36.dp + val controlSm = 28.dp + val controlXl = 56.dp + val radiusFull = 999.dp + val radiusLg = 14.dp + val radiusMd = 10.dp + val radiusSm = 6.dp + val radiusXl = 20.dp + val radiusXs = 4.dp + val space1 = 4.dp + val space10 = 40.dp + val space12 = 48.dp + val space16 = 64.dp + val space2 = 8.dp + val space3 = 12.dp + val space4 = 16.dp + val space5 = 20.dp + val space6 = 24.dp + val space8 = 32.dp + val text2xl = 24.dp + val text3xl = 32.dp + val text4xl = 44.dp + val textBase = 14.dp + val textLg = 17.dp + val textMd = 15.dp + val textSm = 13.dp + val textXl = 20.dp + val textXs = 12.dp +} diff --git a/android/app/src/main/java/app/dudu/ime/DuduImeService.kt b/android/app/src/main/java/app/dudu/ime/DuduImeService.kt new file mode 100644 index 0000000..e127b4e --- /dev/null +++ b/android/app/src/main/java/app/dudu/ime/DuduImeService.kt @@ -0,0 +1,143 @@ +package app.dudu.ime + +import android.content.Intent +import android.inputmethodservice.InputMethodService +import android.view.KeyEvent +import android.view.View +import android.view.inputmethod.EditorInfo +import android.view.inputmethod.InputMethodManager +import androidx.compose.ui.platform.ComposeView +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleRegistry +import androidx.lifecycle.ViewModelStore +import androidx.lifecycle.ViewModelStoreOwner +import androidx.lifecycle.setViewTreeLifecycleOwner +import androidx.lifecycle.setViewTreeViewModelStoreOwner +import androidx.savedstate.SavedStateRegistry +import androidx.savedstate.SavedStateRegistryController +import androidx.savedstate.SavedStateRegistryOwner +import androidx.savedstate.setViewTreeSavedStateRegistryOwner +import app.dudu.app.MainActivity +import app.dudu.design.DuduDesign +import app.dudu.ime.ui.KeyboardScreen +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel + +/** + * dudu 输入法服务。 + * + * 键盘视图选型:**Compose(ComposeView in onCreateInputView)**,理由—— + * 1. 键盘只有一屏自绘 UI(工具条 / 功能键行 / MicBar / partial 条),无传统键位矩阵, + * Compose 的状态驱动渲染 + 动画(波形)比手写 View 省一半代码且不易错; + * 2. 主 App 同为 Compose,设计令牌(DuduTheme/DuduPalette)与组件直接复用; + * 3. IME 场景下 Compose 的已知坑只有一个:ComposeView 依赖 ViewTree*Owner, + * InputMethodService 不是 LifecycleOwner——本类自行实现三个 Owner 并挂到 + * decorView 上(windowRecomposer 从 rootView 向上查找),这是 FlorisBoard 等 + * 开源键盘验证过的稳定方案。 + */ +class DuduImeService : InputMethodService(), + LifecycleOwner, ViewModelStoreOwner, SavedStateRegistryOwner { + + // ─── Owner 实现(Compose in IME 的宿主桥) ─────────────────────────────── + private val lifecycleRegistry = LifecycleRegistry(this) + override val lifecycle: Lifecycle get() = lifecycleRegistry + + private val store = ViewModelStore() + override val viewModelStore: ViewModelStore get() = store + + private val savedStateController = SavedStateRegistryController.create(this) + override val savedStateRegistry: SavedStateRegistry + get() = savedStateController.savedStateRegistry + + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) + private lateinit var controller: KeyboardController + + override fun onCreate() { + super.onCreate() + savedStateController.performRestore(null) + lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE) + + controller = KeyboardController( + context = this, + scope = scope, + commitText = { text -> currentInputConnection?.commitText(text, 1) }, + openMainApp = ::openMainApp, + ) + } + + override fun onCreateInputView(): View { + // windowRecomposer 从 rootView(decorView)向上查找 Owner,必须挂在 decorView + window?.window?.decorView?.let { decor -> + decor.setViewTreeLifecycleOwner(this) + decor.setViewTreeViewModelStoreOwner(this) + decor.setViewTreeSavedStateRegistryOwner(this) + } + lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START) + + return ComposeView(this).apply { + setContent { + DuduDesign { + KeyboardScreen( + controller = controller, + onSwitchIme = ::switchIme, + onText = { t -> currentInputConnection?.commitText(t, 1) }, + onBackspace = { sendDownUpKeyEvents(KeyEvent.KEYCODE_DEL) }, + onEnter = ::performEnter, + onOpenApp = { openMainApp("home") }, + ) + } + } + } + } + + override fun onStartInputView(editorInfo: EditorInfo?, restarting: Boolean) { + super.onStartInputView(editorInfo, restarting) + lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME) + controller.clearNotice() + } + + override fun onFinishInputView(finishingInput: Boolean) { + // 键盘收起:终止进行中的会话(cancel),不上屏 + controller.abort() + lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE) + super.onFinishInputView(finishingInput) + } + + override fun onDestroy() { + controller.abort() + lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY) + scope.cancel() + super.onDestroy() + } + + // ─── 功能键 ────────────────────────────────────────────────────────────── + + /** 发送 = 宿主 return key:优先 editor action,退回 KEYCODE_ENTER。 */ + private fun performEnter() { + val ic = currentInputConnection ?: return + val action = currentInputEditorInfo?.imeOptions?.and(EditorInfo.IME_MASK_ACTION) + if (action != null && action != EditorInfo.IME_ACTION_NONE && action != EditorInfo.IME_ACTION_UNSPECIFIED) { + ic.performEditorAction(action) + } else { + sendDownUpKeyEvents(KeyEvent.KEYCODE_ENTER) + } + } + + /** 切输入法:系统选择器(minSdk 26 下最通用的方式)。 */ + private fun switchIme() { + val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager + imm.showInputMethodPicker() + } + + /** 从键盘打开主 App:登录 / 购买 / 授权引导。 */ + private fun openMainApp(dest: String) { + startActivity( + Intent(this, MainActivity::class.java) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + .putExtra(MainActivity.EXTRA_DEST, dest) + ) + } +} diff --git a/android/app/src/main/java/app/dudu/ime/KeyboardController.kt b/android/app/src/main/java/app/dudu/ime/KeyboardController.kt new file mode 100644 index 0000000..74b0a92 --- /dev/null +++ b/android/app/src/main/java/app/dudu/ime/KeyboardController.kt @@ -0,0 +1,404 @@ +package app.dudu.ime + +import android.Manifest +import android.content.Context +import android.content.pm.PackageManager +import android.os.SystemClock +import android.util.Log +import androidx.core.content.ContextCompat +import app.dudu.BuildConfig +import app.dudu.audio.AudioStreamer +import app.dudu.data.AccountRepository +import app.dudu.data.AccountSnapshot +import app.dudu.data.AccountState +import app.dudu.data.MetricsQueue +import app.dudu.net.AsrSocket +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import java.util.UUID + +/** 键盘 UI 状态(Compose 直接收集渲染)。 */ +data class KeyboardUiState( + val account: AccountSnapshot, + val recording: Boolean = false, + /** 上滑超过取消阈值:MicBar 与提示文案给出"松开取消"视觉反馈 */ + val willCancel: Boolean = false, + /** 已定稿文本(亮色);松开后统一 commit,录音中不进宿主输入框 */ + val finalText: String = "", + /** 流式未定稿文本(灰色) */ + val partialText: String = "", + /** 录音计时(秒),工具条右侧 mono 显示 */ + val seconds: Int = 0, + /** 错误/引导文案,占用 partial 条位置展示 */ + val notice: String? = null, +) + +/** + * 键盘控制器:按住-松开会话状态机,串起 AudioStreamer + AsrSocket + commitText。 + * + * 设计取舍(重要): + * - partial / final 都先进文本条,**松开时才整体 commit**——因为"上滑取消 = 丢弃本次结果", + * 若 final 到达即上屏,取消就无法撤回(InputConnection 无可靠 undo)。 + * - 松开后发 stop,等服务端补发收尾 final(最多 FINALIZE_TIMEOUT_MS),到齐即 commit; + * 超时则降级 commit 当前已有 final + partial。 + */ +class KeyboardController( + private val context: Context, + private val scope: CoroutineScope, + /** final 文本上屏:currentInputConnection.commitText */ + private val commitText: (String) -> Unit, + /** 打开主 App(dest: login / buy / permission) */ + private val openMainApp: (dest: String) -> Unit, +) : AsrSocket.Listener { + + private val repo = AccountRepository.get(context) + private val metrics = MetricsQueue.get(context) + + // ─── 埋点计时基准(事件名对齐 protocol.MetricWhitelist,flush 见 MetricsFlushWorker) ── + private var pressAtMs = 0L // MicBar 按下时刻 + private var releaseAtMs = 0L // 松开时刻 + private var wsConnectAtMs = 0L // WS 发起连接时刻 + private var gotFirstFrame = false // audio.start_ms 已记 + private var gotFirstPartial = false // asr.first_partial_ms 已记 + private var partialCount = 0 + private var sessionErrorCode: String? = null + private var sessionCancelled = false + private var sessionTracked = false + + private val _state = MutableStateFlow(KeyboardUiState(account = repo.snapshot.value)) + val state: StateFlow = _state + + private var socket: AsrSocket? = null + private var audio: AudioStreamer? = null + private var sessionId: String = "" + private var tickerJob: Job? = null + private var finalizeJob: Job? = null + + /** 会话阶段 */ + private enum class Phase { IDLE, RECORDING, FINALIZING } + + private var phase = Phase.IDLE + private var discarded = false // 上滑取消后置位:后续 final 全部丢弃 + + init { + // 账户缓存变化(主 App 切 mock 态 / usage 帧刷新)→ 工具条实时更新 + scope.launch { + repo.snapshot.collect { snap -> + _state.value = _state.value.copy(account = snap) + } + } + } + + // ─── 手势入口 ──────────────────────────────────────────────────────────── + + /** 按下 MicBar。 */ + fun onPressDown() { + when (_state.value.account.state) { + AccountState.GUEST -> { openMainApp("login"); return } + AccountState.QUOTA -> { openMainApp("buy"); return } + else -> Unit + } + if (phase != Phase.IDLE) return + + // IME 内无法弹权限框 → 引导去主 App 授权 + if (ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO) + != PackageManager.PERMISSION_GRANTED + ) { + _state.value = _state.value.copy(notice = "先在 dudu App 开启麦克风权限") + openMainApp("permission") + return + } + + phase = Phase.RECORDING + discarded = false + sessionId = "c-" + UUID.randomUUID() + _state.value = _state.value.copy( + recording = true, willCancel = false, + finalText = "", partialText = "", seconds = 0, notice = null, + ) + + // 埋点基准复位 + pressAtMs = SystemClock.elapsedRealtime() + releaseAtMs = 0L + gotFirstFrame = false + gotFirstPartial = false + partialCount = 0 + sessionErrorCode = null + sessionCancelled = false + sessionTracked = false + + wsConnectAtMs = SystemClock.elapsedRealtime() + socket = AsrSocket(BuildConfig.WS_URL, _state.value.account.token ?: "", repo.deviceId, this) + socket?.connect() + + tickerJob?.cancel() + tickerJob = scope.launch { + while (true) { + delay(1000) + _state.value = _state.value.copy(seconds = _state.value.seconds + 1) + } + } + } + + /** 手指上下移动:dy 为相对按下点的纵向位移(px),上滑为负。 */ + fun onDrag(beyondCancelThreshold: Boolean) { + if (phase != Phase.RECORDING) return + if (_state.value.willCancel != beyondCancelThreshold) { + _state.value = _state.value.copy(willCancel = beyondCancelThreshold) + } + } + + /** 松开(未越过取消阈值)→ stop + 等收尾 final 后上屏。 */ + fun onRelease() { + if (phase != Phase.RECORDING) return + releaseAtMs = SystemClock.elapsedRealtime() + phase = Phase.FINALIZING + stopAudioAndTicker() + socket?.sendStop(sessionId) + _state.value = _state.value.copy(recording = false, willCancel = false) + + // 收尾兜底:超时仍未收到 final → commit 已有内容 + finalizeJob = scope.launch { + delay(FINALIZE_TIMEOUT_MS) + commitAndReset(includePartial = true) + } + } + + /** 松开(已越过取消阈值)→ cancel,丢弃结果(服务端仍计量)。 */ + fun onCancel() { + if (phase != Phase.RECORDING) return + sessionCancelled = true + discarded = true + stopAudioAndTicker() + socket?.sendCancel(sessionId) + teardown() + _state.value = _state.value.copy( + recording = false, willCancel = false, + finalText = "", partialText = "", seconds = 0, + ) + } + + /** 键盘隐藏 / 服务销毁时兜底。 */ + fun abort() { + if (phase == Phase.IDLE) return + sessionCancelled = true + discarded = true + stopAudioAndTicker() + runCatching { socket?.sendCancel(sessionId) } + teardown() + _state.value = _state.value.copy( + recording = false, willCancel = false, + finalText = "", partialText = "", seconds = 0, + ) + } + + fun clearNotice() { + if (_state.value.notice != null) _state.value = _state.value.copy(notice = null) + } + + // ─── AsrSocket.Listener(OkHttp 线程回调 → 切主线程) ───────────────────── + + override fun onOpen() { + scope.launch { + if (phase != Phase.RECORDING) return@launch + metrics.track("ws.connect_ms", mapOf("ms" to SystemClock.elapsedRealtime() - wsConnectAtMs)) + socket?.sendStart(sessionId) + // start 发出后才开始推音频帧 + audio = AudioStreamer( + onFrame = { frame -> + if (!gotFirstFrame) { + gotFirstFrame = true + metrics.track("audio.start_ms", mapOf("ms" to SystemClock.elapsedRealtime() - pressAtMs)) + } + socket?.sendAudio(frame) + }, + onError = { t -> + scope.launch { + Log.w(TAG, "audio error", t) + showError("麦克风不可用,稍后再试") + } + }, + ).also { it.start() } + } + } + + override fun onPartial(sessionId: String, text: String) { + scope.launch { + if (discarded) return@launch + partialCount++ + if (!gotFirstPartial) { + gotFirstPartial = true + metrics.track("asr.first_partial_ms", mapOf("ms" to SystemClock.elapsedRealtime() - pressAtMs)) + } + _state.value = _state.value.copy(partialText = text) + } + } + + override fun onFinal(sessionId: String, text: String) { + scope.launch { + if (discarded) return@launch + _state.value = _state.value.copy( + finalText = _state.value.finalText + text, + partialText = "", + ) + // 松开后等到的收尾 final → 立即上屏并结束会话 + if (phase == Phase.FINALIZING) { + finalizeJob?.cancel() + commitAndReset(includePartial = false) + } + } + } + + override fun onUsage(sessionSeconds: Int, balanceSeconds: Long, trialRemaining: Int) { + scope.launch { + repo.applyUsage(balanceSeconds, trialRemaining) + if (phase == Phase.RECORDING && sessionSeconds > 0) { + // 以服务端实收时长校准本地计时 + _state.value = _state.value.copy(seconds = sessionSeconds) + } + } + } + + override fun onServerError(code: String, message: String) { + scope.launch { + sessionErrorCode = code + // 错误码与文案对齐 server/pkg/protocol/errors.go + when (code) { + "QUOTA_EXCEEDED" -> { + repo.markQuotaExceeded() + // 扣穿前的当句服务端会让说完(final 已到),已识别内容照常上屏 + commitAndReset(includePartial = false) + showError(message.ifEmpty { "今日试用已用完,去购买时长" }) + } + "SESSION_LIMIT" -> { + // 服务端已自动截断定稿:上屏已识别内容 + commitAndReset(includePartial = false) + showError(message.ifEmpty { "单次最长 3 分钟,松开后可继续" }) + } + "UNAUTHORIZED" -> { + repo.markUnauthorized() + discardAndReset() + showError(message.ifEmpty { "登录已失效,请重新登录" }) + } + "RATE_LIMITED" -> { + discardAndReset() + showError(message.ifEmpty { "操作过于频繁,稍后再试" }) + } + else -> { + discardAndReset() + showError(message.ifEmpty { "识别服务暂不可用,稍后再试" }) + } + } + } + } + + override fun onClosed() { + scope.launch { + if (phase == Phase.FINALIZING) { + // 服务端 stop 后直接关连接:把已有内容上屏 + finalizeJob?.cancel() + commitAndReset(includePartial = true) + } + } + } + + override fun onFailure(t: Throwable) { + scope.launch { + Log.w(TAG, "ws failure", t) + if (phase == Phase.IDLE) return@launch + if (phase == Phase.FINALIZING) { + // 断连兜底:已有内容尽量上屏 + finalizeJob?.cancel() + commitAndReset(includePartial = true) + } else { + discardAndReset() + showError("连接中断,松开重试") + } + } + } + + // ─── 内部 ──────────────────────────────────────────────────────────────── + + /** 把 finalText(可选拼上未定稿 partial)commit 到宿主,然后复位。 */ + private fun commitAndReset(includePartial: Boolean) { + if (phase == Phase.IDLE) return + val s = _state.value + val committed = if (includePartial) s.finalText + s.partialText else s.finalText + if (!discarded && committed.isNotEmpty()) { + commitText(committed) + if (releaseAtMs > 0) { + metrics.track( + "asr.release_to_commit_ms", + mapOf("ms" to SystemClock.elapsedRealtime() - releaseAtMs), + ) + } + } + discarded = true // 防止迟到帧重复 commit + stopAudioAndTicker() + teardown() + _state.value = _state.value.copy( + recording = false, willCancel = false, + finalText = "", partialText = "", seconds = 0, + ) + } + + private fun discardAndReset() { + discarded = true + stopAudioAndTicker() + teardown() + _state.value = _state.value.copy( + recording = false, willCancel = false, + finalText = "", partialText = "", seconds = 0, + ) + } + + private fun showError(text: String) { + _state.value = _state.value.copy(notice = text) + // 文案短暂停留后自动消失 + scope.launch { + delay(NOTICE_DISMISS_MS) + if (_state.value.notice == text) clearNotice() + } + } + + private fun stopAudioAndTicker() { + audio?.stop() + audio = null + tickerJob?.cancel() + tickerJob = null + } + + private fun teardown() { + trackSessionEnd() + finalizeJob?.cancel() + finalizeJob = null + socket?.close() + socket = null + phase = Phase.IDLE + } + + /** 会话质量埋点(asr.session),teardown 单点触发、每会话一次。 */ + private fun trackSessionEnd() { + if (sessionTracked || phase == Phase.IDLE) return + sessionTracked = true + metrics.track( + "asr.session", + buildMap { + put("audio_seconds", _state.value.seconds) + put("partial_count", partialCount) + put("cancel", sessionCancelled) + sessionErrorCode?.let { put("error_code", it) } + }, + ) + } + + companion object { + private const val TAG = "DuduKeyboard" + private const val FINALIZE_TIMEOUT_MS = 2000L + private const val NOTICE_DISMISS_MS = 3000L + } +} diff --git a/android/app/src/main/java/app/dudu/ime/ui/KeyboardScreen.kt b/android/app/src/main/java/app/dudu/ime/ui/KeyboardScreen.kt new file mode 100644 index 0000000..6bd6c10 --- /dev/null +++ b/android/app/src/main/java/app/dudu/ime/ui/KeyboardScreen.kt @@ -0,0 +1,269 @@ +package app.dudu.ime.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsPressedAsState +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.draw.scale +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.foundation.text.BasicText +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import app.dudu.data.AccountState +import app.dudu.design.DText +import app.dudu.design.DuduLogoMark +import app.dudu.design.IconArrowUp +import app.dudu.design.IconBackspace +import app.dudu.design.IconCheck +import app.dudu.design.IconEnter +import app.dudu.design.IconGlobe +import app.dudu.design.IconSliders +import app.dudu.design.LocalDuduPalette +import app.dudu.ime.KeyboardController +import app.dudu.ime.KeyboardUiState + +/** + * 键盘面板(设计规范 6.1,自上而下): + * 工具条 → 功能键行 / partial 文本条(录音中替换)→ 大麦克风条 → 取消提示。 + * 面板底色 surface-2 + 顶部 1px border-1,跟随系统 light/dark。 + */ +@Composable +fun KeyboardScreen( + controller: KeyboardController, + onSwitchIme: () -> Unit, + onText: (String) -> Unit, + onBackspace: () -> Unit, + onEnter: () -> Unit, + onOpenApp: () -> Unit, +) { + val p = LocalDuduPalette.current + val ui by controller.state.collectAsState() + + Column( + Modifier + .fillMaxWidth() + .background(p.surface2) + .drawBehind { + drawLine(p.border1, Offset(0f, 0.5f), Offset(size.width, 0.5f), 1f) + } + .padding(start = 8.dp, end = 8.dp, top = 6.dp, bottom = 10.dp) + .navigationBarsPadding(), + ) { + Toolbar(ui, onOpenApp) + + val showTextBar = ui.recording || ui.finalText.isNotEmpty() || ui.partialText.isNotEmpty() + when { + showTextBar -> PartialBar(ui) + ui.notice != null -> NoticeBar(ui.notice!!) + else -> FunctionRow(onSwitchIme, onText, onBackspace, onEnter) + } + + Box(Modifier.padding(horizontal = 2.dp)) { + MicBar( + state = micState(ui), + willCancel = ui.willCancel, + onPressDown = controller::onPressDown, + onDrag = controller::onDrag, + onRelease = controller::onRelease, + onCancel = controller::onCancel, + ) + } + + if (ui.recording) CancelHint(ui.willCancel) + } +} + +private fun micState(ui: KeyboardUiState): MicBarState = when { + ui.account.state == AccountState.GUEST -> MicBarState.Disabled + ui.account.state == AccountState.QUOTA -> MicBarState.Quota + ui.recording -> MicBarState.Recording + else -> MicBarState.Idle +} + +// ─── 工具条:左 logo + 名称,右 账户态文案(设计规范 6.2) ───────────────────── + +@Composable +private fun Toolbar(ui: KeyboardUiState, onOpenApp: () -> Unit) { + val p = LocalDuduPalette.current + Row( + Modifier + .fillMaxWidth() + .padding(start = 6.dp, end = 6.dp, top = 2.dp, bottom = 8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(6.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + DuduLogoMark(13.dp, p.text2) + DText("dudu 语音输入", 12.sp, p.text2) + } + + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + when { + ui.recording -> DText(fmtTimer(ui.seconds), 12.sp, p.positive, mono = true) + ui.account.state == AccountState.GUEST -> DText("未登录", 12.sp, p.text3) + ui.account.state == AccountState.QUOTA -> DText("今日试用已用完", 12.sp, p.warning) + ui.account.state == AccountState.TRIAL -> + DText("试用 · 今日剩 ${fmtDuration(ui.account.trialRemainingSeconds.toLong())}", 12.sp, p.text3) + else -> Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + DText("余额 ${fmtDuration(ui.account.balanceSeconds)}", 12.sp, p.text3) + IconCheck(11.dp, p.text3) + } + } + // 设置入口 → 主 App + val src = remember { MutableInteractionSource() } + IconSliders( + 14.dp, p.text3, + Modifier.clickable(interactionSource = src, indication = null, onClick = onOpenApp), + ) + } + } +} + +private fun fmtTimer(s: Int): String = "%02d:%02d".format(s / 60, s % 60) + +private fun fmtDuration(seconds: Long): String = + if (seconds >= 60) "${seconds / 60} 分钟" else "$seconds 秒" + +// ─── partial 文本条:深色玻璃(硬规则),final 亮 / partial 灰拼接 ───────────── + +@Composable +private fun PartialBar(ui: KeyboardUiState) { + val p = LocalDuduPalette.current + val text: AnnotatedString = buildAnnotatedString { + if (ui.finalText.isEmpty() && ui.partialText.isEmpty()) { + withStyle(SpanStyle(color = p.overlayText2)) { append("聆听中…") } + } else { + withStyle(SpanStyle(color = p.overlayText)) { append(ui.finalText) } + withStyle(SpanStyle(color = p.overlayText2)) { append(ui.partialText) } + } + } + Box( + Modifier + .fillMaxWidth() + .padding(start = 2.dp, end = 2.dp, bottom = 8.dp) + .defaultMinSize(minHeight = 40.dp) + .clip(RoundedCornerShape(10.dp)) + .background(p.overlayBg) + .padding(horizontal = 12.dp, vertical = 9.dp), + contentAlignment = Alignment.CenterStart, + ) { + BasicText(text = text, style = TextStyle(fontSize = 15.sp, lineHeight = 22.5.sp)) + } +} + +/** 错误/引导文案:占 partial 条位置,同样深色玻璃,warning 色文字。 */ +@Composable +private fun NoticeBar(notice: String) { + val p = LocalDuduPalette.current + Box( + Modifier + .fillMaxWidth() + .padding(start = 2.dp, end = 2.dp, bottom = 8.dp) + .defaultMinSize(minHeight = 40.dp) + .clip(RoundedCornerShape(10.dp)) + .background(p.overlayBg) + .padding(horizontal = 12.dp, vertical = 9.dp), + contentAlignment = Alignment.CenterStart, + ) { + DText(notice, 15.sp, p.warning, lineHeight = 22.5.sp) + } +} + +// ─── 功能键行:切输入法 / , / 。 / 退格 / 发送(不做 26 键,语音为主) ───────── + +@Composable +private fun FunctionRow( + onSwitchIme: () -> Unit, + onText: (String) -> Unit, + onBackspace: () -> Unit, + onEnter: () -> Unit, +) { + val p = LocalDuduPalette.current + Row( + Modifier + .fillMaxWidth() + .padding(start = 2.dp, end = 2.dp, bottom = 8.dp), + horizontalArrangement = Arrangement.spacedBy(6.dp), + ) { + FnKey(flex = 1.3f, onClick = onSwitchIme) { IconGlobe(17.dp, p.text1) } + FnKey(flex = 1f, onClick = { onText(",") }) { DText(",", 15.sp, p.text1) } + FnKey(flex = 1f, onClick = { onText("。") }) { DText("。", 15.sp, p.text1) } + FnKey(flex = 1.3f, onClick = onBackspace) { IconBackspace(17.dp, p.text1) } + FnKey(flex = 1.3f, onClick = onEnter) { IconEnter(16.dp, p.text1) } + } +} + +@Composable +private fun RowScope.FnKey( + flex: Float, + onClick: () -> Unit, + content: @Composable () -> Unit, +) { + val p = LocalDuduPalette.current + val src = remember { MutableInteractionSource() } + val pressed by src.collectIsPressedAsState() + Box( + Modifier + .weight(flex) + .height(40.dp) + .scale(if (pressed) 0.97f else 1f) // 键帽按压缩 0.97(设计规范) + .clip(RoundedCornerShape(6.dp)) + .background(if (pressed) p.surface3 else p.surfaceCard) + .clickable(interactionSource = src, indication = null, onClick = onClick), + contentAlignment = Alignment.Center, + ) { + content() + } +} + +// ─── 取消提示(仅录音中):"↑ 上滑取消",越过阈值转 danger"松开取消" ─────────── + +@Composable +private fun CancelHint(willCancel: Boolean) { + val p = LocalDuduPalette.current + val color = if (willCancel) p.danger else p.text3 + Row( + Modifier + .fillMaxWidth() + .padding(top = 6.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally), + verticalAlignment = Alignment.CenterVertically, + ) { + IconArrowUp(11.dp, color) + DText(if (willCancel) "松开取消" else "上滑取消", 12.sp, color) + } +} diff --git a/android/app/src/main/java/app/dudu/ime/ui/MicBar.kt b/android/app/src/main/java/app/dudu/ime/ui/MicBar.kt new file mode 100644 index 0000000..4ee063d --- /dev/null +++ b/android/app/src/main/java/app/dudu/ime/ui/MicBar.kt @@ -0,0 +1,122 @@ +package app.dudu.ime.ui + +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.scale +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.foundation.gestures.awaitEachGesture +import androidx.compose.foundation.gestures.awaitFirstDown +import app.dudu.design.DText +import app.dudu.design.IconMic +import app.dudu.design.LocalDuduPalette + +/** MicBar 四态(设计规范 3.3,文案内置逐字对照 MicBar.jsx)。 */ +enum class MicBarState { Idle, Recording, Disabled, Quota } + +/** + * 大麦克风条:高 56 / 全宽 / 胶囊 999 / 15sp 600。 + * 手势:按下开始 → 上滑超过 48dp 进入"将取消"(视觉转 danger + 文案"松开取消")→ + * 松开按是否越阈值分流 onRelease / onCancel。 + */ +@Composable +fun MicBar( + state: MicBarState, + willCancel: Boolean, + onPressDown: () -> Unit, + onDrag: (beyondCancelThreshold: Boolean) -> Unit, + onRelease: () -> Unit, + onCancel: () -> Unit, + modifier: Modifier = Modifier, +) { + val p = LocalDuduPalette.current + var pressed by remember { mutableStateOf(false) } + + val bg by animateColorAsState( + targetValue = when { + state == MicBarState.Disabled -> p.surface3 + state == MicBarState.Quota -> p.warning + state == MicBarState.Recording && willCancel -> p.danger + state == MicBarState.Recording -> p.positive + pressed -> p.accentPress + else -> p.accent + }, + animationSpec = tween(120), + label = "micbar-bg", + ) + // 移动端按压缩 0.97(设计规范) + val scale by animateFloatAsState( + targetValue = if (pressed && state == MicBarState.Idle) 0.97f else 1f, + animationSpec = tween(120), + label = "micbar-scale", + ) + + Row( + modifier = modifier + .fillMaxWidth() + .height(56.dp) + .scale(scale) + .clip(RoundedCornerShape(999.dp)) + .background(bg) + .pointerInput(state) { + // 等价 touch-action:none——独占整条手势,竖向位移用于取消判定而非滚动 + awaitEachGesture { + val down = awaitFirstDown() + down.consume() + pressed = true + onPressDown() + val cancelPx = 48.dp.toPx() + var beyond = false + while (true) { + val event = awaitPointerEvent() + val change = event.changes.firstOrNull { it.id == down.id } ?: break + if (!change.pressed) break + beyond = (change.position.y - down.position.y) < -cancelPx + onDrag(beyond) + change.consume() + } + pressed = false + if (beyond) onCancel() else onRelease() + } + }, + horizontalArrangement = Arrangement.spacedBy(10.dp, Alignment.CenterHorizontally), + verticalAlignment = Alignment.CenterVertically, + ) { + when (state) { + MicBarState.Idle -> { + IconMic(20.dp, p.textOnAccent) + DText("按住说话", 15.sp, p.textOnAccent, weight = FontWeight.SemiBold) + } + MicBarState.Recording -> { + Waveform(bars = 9, height = 18.dp, color = p.textOnAccent.copy(alpha = 0.95f)) + DText( + if (willCancel) "松开取消" else "松开完成 · 上滑取消", + 15.sp, p.textOnAccent, weight = FontWeight.SemiBold, + ) + } + MicBarState.Disabled -> { + DText("打开 dudu App 登录", 15.sp, p.text3, weight = FontWeight.SemiBold) + } + MicBarState.Quota -> { + DText("今日试用已用完,去购买时长", 15.sp, p.textOnAccent, weight = FontWeight.SemiBold) + } + } + } +} diff --git a/android/app/src/main/java/app/dudu/ime/ui/Waveform.kt b/android/app/src/main/java/app/dudu/ime/ui/Waveform.kt new file mode 100644 index 0000000..61c64b3 --- /dev/null +++ b/android/app/src/main/java/app/dudu/ime/ui/Waveform.kt @@ -0,0 +1,78 @@ +package app.dudu.ime.ui + +import androidx.compose.animation.core.CubicBezierEasing +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.StartOffset +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +/** + * 波形(设计规范 3.2):条宽 3 / 间距 3 / 圆角 2,固定 12 值高度序列, + * active 时 scaleY 0.35→1 交替 0.9s ease-in-out、逐条延迟 (i%6)*0.07s。 + * 这是全产品唯一循环动画,仅录音中出现。 + */ +private val PATTERN = floatArrayOf(0.3f, 0.55f, 0.85f, 0.45f, 0.7f, 1f, 0.4f, 0.6f, 0.25f, 0.8f, 0.5f, 0.35f) +private val EaseInOut = CubicBezierEasing(0.42f, 0f, 0.58f, 1f) + +@Composable +fun Waveform( + bars: Int, + height: Dp, + color: Color, + modifier: Modifier = Modifier, + active: Boolean = true, +) { + val n = bars.coerceIn(1, PATTERN.size) + val transition = rememberInfiniteTransition(label = "waveform") + val scales: List> = (0 until n).map { i -> + if (active) { + transition.animateFloat( + initialValue = 0.35f, + targetValue = 1f, + animationSpec = infiniteRepeatable( + animation = tween(durationMillis = 900, easing = EaseInOut), + repeatMode = RepeatMode.Reverse, + initialStartOffset = StartOffset((i % 6) * 70), + ), + label = "bar$i", + ) + } else { + remember { mutableFloatStateOf(1f) } + } + } + + val barW = 3.dp + val gap = 3.dp + val totalW = barW * n + gap * (n - 1) + + Canvas(modifier.size(totalW, height)) { + val bw = barW.toPx() + val g = gap.toPx() + val h = size.height + for (i in 0 until n) { + val base = (PATTERN[i] * h).coerceAtLeast(4.dp.toPx()) + val bh = base * scales[i].value + drawRoundRect( + color = color, + topLeft = Offset(i * (bw + g), (h - bh) / 2f), + size = Size(bw, bh), + cornerRadius = CornerRadius(2.dp.toPx()), + ) + } + } +} diff --git a/android/app/src/main/java/app/dudu/net/AsrSocket.kt b/android/app/src/main/java/app/dudu/net/AsrSocket.kt new file mode 100644 index 0000000..d3e51ef --- /dev/null +++ b/android/app/src/main/java/app/dudu/net/AsrSocket.kt @@ -0,0 +1,106 @@ +package app.dudu.net + +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import okhttp3.WebSocket +import okhttp3.WebSocketListener +import okio.ByteString.Companion.toByteString +import org.json.JSONObject +import java.util.concurrent.TimeUnit + +/** + * WS 客户端,协议对齐 server/pkg/protocol/ws.go(WSS /v1/asr/stream): + * - 握手 header:Authorization: Bearer 、X-Device-ID + * - 上行文本帧:{"type":"start","session_id":...,"sample_rate":16000,"format":"pcm16"} / stop / cancel + * - 上行二进制帧:PCM 16k/16bit/mono,100ms = 3200B + * - 下行文本帧:partial / final / usage / error + * + * MVP:每次按住-松开新建一条连接(简单可靠)。 + * TODO(latency): 改为常驻连接 + ping/pong 保活,按键零握手(见 backend-architecture 第四章)。 + */ +class AsrSocket( + private val wsUrl: String, + private val token: String, + private val deviceId: String, + private val listener: Listener, +) { + interface Listener { + fun onOpen() + fun onPartial(sessionId: String, text: String) + fun onFinal(sessionId: String, text: String) + fun onUsage(sessionSeconds: Int, balanceSeconds: Long, trialRemaining: Int) + fun onServerError(code: String, message: String) + fun onClosed() + fun onFailure(t: Throwable) + } + + private var ws: WebSocket? = null + + fun connect() { + val req = Request.Builder() + .url(wsUrl) + .header("Authorization", "Bearer $token") + .header("X-Device-ID", deviceId) + .build() + ws = client.newWebSocket(req, object : WebSocketListener() { + override fun onOpen(webSocket: WebSocket, response: Response) = listener.onOpen() + + override fun onMessage(webSocket: WebSocket, text: String) { + val msg = runCatching { JSONObject(text) }.getOrNull() ?: return + val sid = msg.optString("session_id") + when (msg.optString("type")) { + "partial" -> listener.onPartial(sid, msg.optString("text")) + "final" -> listener.onFinal(sid, msg.optString("text")) + "usage" -> listener.onUsage( + msg.optInt("session_seconds"), + msg.optLong("balance_seconds"), + msg.optInt("trial_remaining"), + ) + "error" -> listener.onServerError(msg.optString("code"), msg.optString("message")) + } + } + + override fun onClosed(webSocket: WebSocket, code: Int, reason: String) = listener.onClosed() + + override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) = + listener.onFailure(t) + }) + } + + fun sendStart(sessionId: String) { + val json = JSONObject() + .put("type", "start") + .put("session_id", sessionId) + .put("sample_rate", 16000) + .put("format", "pcm16") + ws?.send(json.toString()) + } + + fun sendAudio(frame: ByteArray) { + ws?.send(frame.toByteString(0, frame.size)) + } + + fun sendStop(sessionId: String) { + ws?.send(JSONObject().put("type", "stop").put("session_id", sessionId).toString()) + } + + fun sendCancel(sessionId: String) { + ws?.send(JSONObject().put("type", "cancel").put("session_id", sessionId).toString()) + } + + fun close() { + ws?.close(1000, null) + ws = null + } + + companion object { + // 进程级单例 client:连接池/线程池复用 + private val client: OkHttpClient by lazy { + OkHttpClient.Builder() + .connectTimeout(5, TimeUnit.SECONDS) + .pingInterval(20, TimeUnit.SECONDS) + .build() + } + } +} diff --git a/android/app/src/main/java/app/dudu/net/DuduApi.kt b/android/app/src/main/java/app/dudu/net/DuduApi.kt new file mode 100644 index 0000000..410e67a --- /dev/null +++ b/android/app/src/main/java/app/dudu/net/DuduApi.kt @@ -0,0 +1,262 @@ +package app.dudu.net + +import app.dudu.BuildConfig +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.MultipartBody +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import org.json.JSONArray +import org.json.JSONObject +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.util.concurrent.TimeUnit +import java.util.zip.GZIPOutputStream + +/** REST 错误:服务端统一错误体 {"code","message"}(见 server/pkg/protocol/errors.go)。 */ +class ApiException(val code: String, message: String) : Exception(message) { + companion object { + const val NETWORK = "NETWORK" // 客户端本地网络错误(非服务端码) + } +} + +/** + * REST 客户端,契约逐一对齐 server/pkg/protocol/dto.go 与 internal/httpapi/router.go: + * - POST /v1/auth/wechat 微信 code 换 JWT(AuthWechatRequest → AuthTokenResponse) + * - GET /v1/me MeResponse + * - GET /v1/packs PacksResponse + * - POST /v1/orders CreateOrderRequest(channel=app) → CreateOrderResponse + * - GET /v1/orders/{id} OrderStatusResponse + * - POST /v1/feedback multipart(content / images[] / diagnostics) + * - POST /v1/metrics/batch MetricsBatchRequest(gzip,可匿名) + * + * 选型:OkHttp + org.json(与 AsrSocket 一致),不引 Retrofit/Moshi——接口仅 8 个, + * 手写解析依赖最小且字段对齐一目了然。 + */ +object DuduApi { + + // TODO(release): 生产切 https://api.dudu.app —— 在 app/build.gradle.kts 按 + // buildType 注入不同 BuildConfig.API_BASE,此处无需改动。 + private val base = BuildConfig.API_BASE.trimEnd('/') + + private val client: OkHttpClient by lazy { + OkHttpClient.Builder() + .connectTimeout(5, TimeUnit.SECONDS) + .callTimeout(60, TimeUnit.SECONDS) + .build() + } + + // ─── DTO(字段对齐 dto.go) ────────────────────────────────────────────── + + data class UserInfo(val userId: String, val nicknameMasked: String) + + data class LoginResult(val token: String, val user: UserInfo) + + data class Me( + val userId: String, + val nicknameMasked: String, + val balanceSeconds: Long, + val trialDailyLimit: Int, // 秒(服务端 TrialDailySeconds) + val trialUsedToday: Int, // 秒 + val accountState: String, // ok | trial | quota + ) + + data class Pack( + val id: String, + val minutes: Int, + val priceCents: Int, + val unitDesc: String, + val tag: String?, // "省 13%" 等,可空 + ) + + data class OrderCreated(val orderId: String, val payParams: Map) + + data class OrderStatus(val status: String, val balanceSeconds: Long) // pending | paid | closed + + // ─── 接口 ──────────────────────────────────────────────────────────────── + + /** POST /v1/auth/wechat:移动端 OpenSDK code 换 JWT。 */ + suspend fun loginWechat(code: String): LoginResult { + val req = jsonRequest("/v1/auth/wechat", null, JSONObject().put("code", code)) + return exec(req) { body -> + val j = JSONObject(body) + val u = j.getJSONObject("user") + LoginResult( + token = j.getString("token"), + user = UserInfo(u.getString("user_id"), u.optString("nickname_masked")), + ) + } + } + + /** POST /v1/auth/logout:JWT 进黑名单(失败静默,本地照常清态)。 */ + suspend fun logout(token: String) { + val req = jsonRequest("/v1/auth/logout", token, JSONObject()) + runCatching { exec(req) { } } + } + + /** GET /v1/me。 */ + suspend fun me(token: String): Me { + val req = getRequest("/v1/me", token) + return exec(req) { body -> + val j = JSONObject(body) + Me( + userId = j.getString("user_id"), + nicknameMasked = j.optString("nickname_masked"), + balanceSeconds = j.optLong("balance_seconds"), + trialDailyLimit = j.optInt("trial_daily_limit"), + trialUsedToday = j.optInt("trial_used_today"), + accountState = j.optString("account_state"), + ) + } + } + + /** GET /v1/packs(无需登录,服务端可配)。 */ + suspend fun packs(): List { + val req = getRequest("/v1/packs", null) + return exec(req) { body -> + val arr = JSONObject(body).getJSONArray("packs") + (0 until arr.length()).map { i -> + val p = arr.getJSONObject(i) + Pack( + id = p.getString("id"), + minutes = p.getInt("minutes"), + priceCents = p.getInt("price_cents"), + unitDesc = p.optString("unit_desc"), + tag = p.optString("tag").ifEmpty { null }, + ) + } + } + } + + /** POST /v1/orders:channel 固定 app(移动收银台)→ pay_params。 */ + suspend fun createOrder(token: String, packId: String): OrderCreated { + val req = jsonRequest( + "/v1/orders", token, + JSONObject().put("pack_id", packId).put("channel", "app"), + ) + return exec(req) { body -> + val j = JSONObject(body) + val params = mutableMapOf() + j.optJSONObject("pay_params")?.let { pp -> + pp.keys().forEach { k -> params[k] = pp.get(k) } + } + OrderCreated(j.getString("order_id"), params) + } + } + + /** GET /v1/orders/{id}:轮询至 paid。 */ + suspend fun orderStatus(token: String, orderId: String): OrderStatus { + val req = getRequest("/v1/orders/$orderId", token) + return exec(req) { body -> + val j = JSONObject(body) + OrderStatus(j.getString("status"), j.optLong("balance_seconds")) + } + } + + /** + * POST /v1/feedback(multipart,需登录)。 + * images:原始字节(≤3 张、单张 ≤5MB,调用方已校验),服务端按 magic bytes 验格式。 + */ + suspend fun submitFeedback( + token: String, + content: String, + images: List, + diagnostics: String?, + ): String { + val mb = MultipartBody.Builder().setType(MultipartBody.FORM) + .addFormDataPart("content", content) + if (diagnostics != null) mb.addFormDataPart("diagnostics", diagnostics) + images.forEachIndexed { i, data -> + val (ext, mime) = sniffImage(data) ?: ("jpg" to "image/jpeg") + mb.addFormDataPart("images[]", "${i + 1}.$ext", data.toRequestBody(mime.toMediaType())) + } + val req = Request.Builder() + .url(base + "/v1/feedback") + .header("Authorization", "Bearer $token") + .header("X-Platform", "android") + .header("X-App-Version", BuildConfig.VERSION_NAME) + .post(mb.build()) + .build() + return exec(req) { body -> JSONObject(body).getString("feedback_id") } + } + + /** POST /v1/metrics/batch(gzip JSON,可匿名):返回是否成功,永不抛错。 */ + suspend fun metricsBatch(bodyJson: String): Boolean = withContext(Dispatchers.IO) { + val gz = ByteArrayOutputStream() + GZIPOutputStream(gz).use { it.write(bodyJson.toByteArray(Charsets.UTF_8)) } + val req = Request.Builder() + .url(base + "/v1/metrics/batch") + .header("Content-Encoding", "gzip") + .post(gz.toByteArray().toRequestBody(JSON)) + .build() + runCatching { client.newCall(req).execute().use { it.isSuccessful } }.getOrDefault(false) + } + + /** + * POST /v1/pay/notify —— **仅 Mock 支付链路使用**:模拟微信支付回调,让 dev server + * 把订单置为 paid(回调体对齐 server billing.MockPay.ParseNotify 的明文 JSON)。 + * TODO(wechat-pay): 真实微信支付接入后删除,回调由微信服务器直达后端。 + */ + suspend fun mockPayNotify(orderId: String, amountCents: Int): Boolean = withContext(Dispatchers.IO) { + val body = JSONObject() + .put("order_id", orderId) + .put("transaction_id", "mock-tx-" + System.currentTimeMillis()) + .put("amount_cents", amountCents) + .toString() + val req = Request.Builder() + .url(base + "/v1/pay/notify") + .post(body.toRequestBody(JSON)) + .build() + runCatching { client.newCall(req).execute().use { it.isSuccessful } }.getOrDefault(false) + } + + // ─── 内部 ──────────────────────────────────────────────────────────────── + + private val JSON = "application/json; charset=utf-8".toMediaType() + + private fun getRequest(path: String, token: String?): Request = + Request.Builder().url(base + path).apply { + if (token != null) header("Authorization", "Bearer $token") + }.build() + + private fun jsonRequest(path: String, token: String?, body: JSONObject): Request = + Request.Builder().url(base + path).apply { + if (token != null) header("Authorization", "Bearer $token") + }.post(body.toString().toRequestBody(JSON)).build() + + private suspend fun exec(req: Request, parse: (String) -> T): T = withContext(Dispatchers.IO) { + val resp = try { + client.newCall(req).execute() + } catch (e: IOException) { + throw ApiException(ApiException.NETWORK, "网络连接失败,检查网络后再试") + } + resp.use { + val body = it.body?.string().orEmpty() + if (!it.isSuccessful) { + val err = runCatching { JSONObject(body) }.getOrNull() + val code = err?.optString("code").orEmpty().ifEmpty { "HTTP_${it.code}" } + val msg = err?.optString("message").orEmpty().ifEmpty { "请求失败,稍后再试" } + throw ApiException(code, msg) + } + try { + parse(body) + } catch (e: org.json.JSONException) { + throw ApiException(ApiException.NETWORK, "响应解析失败,稍后再试") + } + } + } + + /** magic bytes 判断图片格式(与服务端 feedback.sniffImage 一致)。 */ + fun sniffImage(data: ByteArray): Pair? = when { + data.size > 3 && data[0] == 0xFF.toByte() && data[1] == 0xD8.toByte() && data[2] == 0xFF.toByte() -> + "jpg" to "image/jpeg" + data.size > 4 && data[0] == 0x89.toByte() && data[1] == 'P'.code.toByte() && + data[2] == 'N'.code.toByte() && data[3] == 'G'.code.toByte() -> + "png" to "image/png" + data.size > 12 && String(data, 0, 4) == "RIFF" && String(data, 8, 4) == "WEBP" -> + "webp" to "image/webp" + else -> null + } +} diff --git a/android/app/src/main/java/app/dudu/pay/PayClient.kt b/android/app/src/main/java/app/dudu/pay/PayClient.kt new file mode 100644 index 0000000..5344cfd --- /dev/null +++ b/android/app/src/main/java/app/dudu/pay/PayClient.kt @@ -0,0 +1,44 @@ +package app.dudu.pay + +import app.dudu.net.DuduApi +import kotlinx.coroutines.delay + +/** 支付结果。 */ +sealed class PayResult { + data object Success : PayResult() + data object Cancelled : PayResult() + data class Failed(val reason: String) : PayResult() +} + +/** 微信 APP 支付抽象:拉起收银台,返回用户侧支付结果(最终以服务端订单态为准)。 */ +interface PayClient { + /** + * @param orderId 服务端订单号(mock 链路用;真实微信支付只用 payParams) + * @param payParams POST /v1/orders 返回的 app 支付参数(appid/partnerid/prepayid/...) + */ + suspend fun pay(orderId: String, payParams: Map): PayResult +} + +/** + * Mock 实现:延迟 2 秒模拟用户在微信收银台完成支付,然后调 dev server 的 + * POST /v1/pay/notify(明文 mock 回调)把订单置为 paid——这样上层"轮询订单至 paid" + * 的真实链路完整可走。 + * + * TODO(wechat-pay): 用户拿到 2B 商户号后替换为微信 OpenSDK 实现: + * 1. IWXAPI.sendReq(PayReq().apply { appId/partnerId/prepayId/packageValue/ + * nonceStr/timeStamp/sign = payParams[...] }) + * 2. 新建 wxapi/WXPayEntryActivity,onResp 的 BaseResp.errCode: + * 0 → Success;-2 → Cancelled;其余 → Failed + * 3. 服务端回调由微信直达 /v1/pay/notify,删除 DuduApi.mockPayNotify + */ +class MockPayClient : PayClient { + override suspend fun pay(orderId: String, payParams: Map): PayResult { + delay(2000) // 模拟收银台停留 + val amount = (payParams["amount"] as? Number)?.toInt() ?: 0 + DuduApi.mockPayNotify(orderId, amount) // 触发 dev server 入账(失败也继续,轮询会给出结论) + return PayResult.Success + } +} + +/** 装配点:换真实 SDK 时只改这一行。 */ +val payClient: PayClient = MockPayClient() diff --git a/android/app/src/main/java/app/dudu/wechat/WechatAuth.kt b/android/app/src/main/java/app/dudu/wechat/WechatAuth.kt new file mode 100644 index 0000000..b77af50 --- /dev/null +++ b/android/app/src/main/java/app/dudu/wechat/WechatAuth.kt @@ -0,0 +1,31 @@ +package app.dudu.wechat + +import kotlinx.coroutines.delay + +/** 微信授权抽象:拉起微信,回传 OpenSDK 的 auth code。 */ +interface WechatAuth { + /** @return 微信授权 code(用于 POST /v1/auth/wechat 换 JWT) */ + suspend fun requestCode(): String +} + +/** + * Mock 实现:直接返回假 code。dev server 未配置微信凭证时走 mock 微信, + * 任意 code 都会建号并签发 JWT,因此登录链路可端到端联调。 + * + * TODO(wechat-sdk): 用户拿到 2A 凭证(AppID)后替换为微信 OpenSDK 实现: + * 1. 依赖 com.tencent.mm.opensdk:wechat-sdk-android + * 2. WXAPIFactory.createWXAPI(context, APP_ID).sendReq(SendAuth.Req().apply { + * scope = "snsapi_userinfo"; state = "dudu_login" + * }) + * 3. 新建 wxapi/WXEntryActivity,在 onResp 中取 SendAuth.Resp.code 回传 + * (suspendCancellableCoroutine 桥接回调) + */ +class MockWechatAuth : WechatAuth { + override suspend fun requestCode(): String { + delay(400) // 模拟拉起微信授权页往返 + return "mock-code-" + System.currentTimeMillis() + } +} + +/** 装配点:换真实 SDK 时只改这一行。 */ +val wechatAuth: WechatAuth = MockWechatAuth() diff --git a/android/app/src/main/res/drawable/ic_launcher_foreground.xml b/android/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..13dfd45 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,32 @@ + + + + + + + + diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..a8a8fa5 --- /dev/null +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/android/app/src/main/res/values-night/colors.xml b/android/app/src/main/res/values-night/colors.xml new file mode 100644 index 0000000..f2dedab --- /dev/null +++ b/android/app/src/main/res/values-night/colors.xml @@ -0,0 +1,4 @@ + + + #0F1116 + diff --git a/android/app/src/main/res/values-night/themes.xml b/android/app/src/main/res/values-night/themes.xml new file mode 100644 index 0000000..74e7983 --- /dev/null +++ b/android/app/src/main/res/values-night/themes.xml @@ -0,0 +1,9 @@ + + + + diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..0e2748f --- /dev/null +++ b/android/app/src/main/res/values/colors.xml @@ -0,0 +1,6 @@ + + + + #4F6EF7 + #F6F7FA + diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..3bc4770 --- /dev/null +++ b/android/app/src/main/res/values/strings.xml @@ -0,0 +1,5 @@ + + + dudu + dudu 语音输入 + diff --git a/android/app/src/main/res/values/themes.xml b/android/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..4e45f99 --- /dev/null +++ b/android/app/src/main/res/values/themes.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/android/app/src/main/res/xml/method.xml b/android/app/src/main/res/xml/method.xml new file mode 100644 index 0000000..c68dc36 --- /dev/null +++ b/android/app/src/main/res/xml/method.xml @@ -0,0 +1,9 @@ + + + + diff --git a/android/build.gradle.kts b/android/build.gradle.kts new file mode 100644 index 0000000..d930314 --- /dev/null +++ b/android/build.gradle.kts @@ -0,0 +1,6 @@ +// dudu Android(主 App + IME Service 单 module) +plugins { + alias(libs.plugins.android.application) apply false + alias(libs.plugins.kotlin.android) apply false + alias(libs.plugins.kotlin.compose) apply false +} diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000..73403e4 --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,4 @@ +org.gradle.jvmargs=-Xmx3g -Dfile.encoding=UTF-8 +android.useAndroidX=true +android.nonTransitiveRClass=true +kotlin.code.style=official diff --git a/android/gradle/libs.versions.toml b/android/gradle/libs.versions.toml new file mode 100644 index 0000000..a98c375 --- /dev/null +++ b/android/gradle/libs.versions.toml @@ -0,0 +1,29 @@ +[versions] +agp = "8.11.1" +kotlin = "2.2.20" +coreKtx = "1.16.0" +activityCompose = "1.10.1" +lifecycle = "2.9.1" +savedstate = "1.3.0" +composeBom = "2025.06.01" +okhttp = "4.12.0" +coroutines = "1.10.2" +work = "2.10.1" + +[libraries] +androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } +androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } +androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle" } +androidx-lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycle" } +androidx-savedstate-ktx = { group = "androidx.savedstate", name = "savedstate-ktx", version.ref = "savedstate" } +compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } +compose-ui = { group = "androidx.compose.ui", name = "ui" } +compose-foundation = { group = "androidx.compose.foundation", name = "foundation" } +okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" } +kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "coroutines" } +androidx-work-runtime-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "work" } + +[plugins] +android-application = { id = "com.android.application", version.ref = "agp" } +kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } diff --git a/android/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..13372aef5e24af05341d49695ee84e5f9b594659 GIT binary patch literal 53636 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ⅅ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 H-+%o-(+5aQ literal 0 HcmV?d00001 diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..e4ef43f --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip diff --git a/android/gradlew b/android/gradlew new file mode 100755 index 0000000..9d82f78 --- /dev/null +++ b/android/gradlew @@ -0,0 +1,160 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/android/gradlew.bat b/android/gradlew.bat new file mode 100644 index 0000000..8a0b282 --- /dev/null +++ b/android/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts new file mode 100644 index 0000000..675de17 --- /dev/null +++ b/android/settings.gradle.kts @@ -0,0 +1,18 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "dudu-android" +include(":app") diff --git a/design-pipeline/export-tokens.mjs b/design-pipeline/export-tokens.mjs new file mode 100644 index 0000000..4942e87 --- /dev/null +++ b/design-pipeline/export-tokens.mjs @@ -0,0 +1,193 @@ +#!/usr/bin/env node +/** + * export-tokens.mjs — 设计令牌导出管线(零依赖,Node >= 18) + * + * 解析 design/tokens/*.css 的 CSS 自定义属性(:root = light,[data-theme="dark"] = dark), + * 生成移动端原生主题文件,保证三端令牌单一来源: + * generated/ios/DuduTheme.swift — Color(light/dark 动态) + 尺寸/字号常量 + * generated/android/DuduTheme.kt — Compose Color/Dp/Sp 常量(Light/Dark 两组) + * + * 用法:node design-pipeline/export-tokens.mjs [--check] + * --check CI 校验模式:重新生成并与已提交产物比对,不一致退出 1 + */ + +import { readFileSync, writeFileSync, mkdirSync, readdirSync, existsSync } from 'node:fs'; +import { resolve, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dir = dirname(fileURLToPath(import.meta.url)); +const TOKENS_DIR = resolve(__dir, '../design/tokens'); +const OUT_IOS = resolve(__dir, 'generated/ios/DuduTheme.swift'); +const OUT_ANDROID = resolve(__dir, 'generated/android/DuduTheme.kt'); + +// ─── 解析 ──────────────────────────────────────────────────────────────────── + +/** 提取一个 CSS 块(selector { ... })里的全部 --var: value */ +function parseBlock(css, selector) { + const out = {}; + const re = new RegExp(`${selector.replace(/[[\]"=]/g, (m) => '\\' + m)}\\s*\\{([\\s\\S]*?)\\}`, 'g'); + let m; + while ((m = re.exec(css))) { + const body = m[1].replace(/\/\*[\s\S]*?\*\//g, ''); + for (const line of body.split(';')) { + const mm = line.match(/--([\w-]+)\s*:\s*(.+)/); + if (mm) out[mm[1].trim()] = mm[2].trim(); + } + } + return out; +} + +const files = readdirSync(TOKENS_DIR).filter((f) => f.endsWith('.css')); +let light = {}, dark = {}; +for (const f of files) { + const css = readFileSync(resolve(TOKENS_DIR, f), 'utf8'); + Object.assign(light, parseBlock(css, ':root')); + Object.assign(dark, parseBlock(css, '[data-theme="dark"]')); +} + +/** 解析 var() 引用链到最终值 */ +function resolveVar(val, scope) { + let v = val, guard = 0; + while (v && v.includes('var(') && guard++ < 10) { + v = v.replace(/var\(--([\w-]+)\)/g, (_, name) => scope[name] ?? light[name] ?? ''); + } + return v.trim(); +} + +// ─── 颜色转换 ───────────────────────────────────────────────────────────────── + +/** #RGB/#RRGGBB/rgba() → {r,g,b,a} 0-1 浮点;无法解析返回 null */ +function parseColor(v) { + if (!v) return null; + v = v.trim(); + let m = v.match(/^#([0-9a-f]{6})$/i); + if (m) { + const n = parseInt(m[1], 16); + return { r: (n >> 16 & 255) / 255, g: (n >> 8 & 255) / 255, b: (n & 255) / 255, a: 1 }; + } + m = v.match(/^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*(?:,\s*([\d.]+)\s*)?\)$/); + if (m) return { r: +m[1] / 255, g: +m[2] / 255, b: +m[3] / 255, a: m[4] === undefined ? 1 : +m[4] }; + return null; +} + +const hex2 = (f) => Math.round(f * 255).toString(16).padStart(2, '0').toUpperCase(); + +// ─── 分类 ──────────────────────────────────────────────────────────────────── + +const colorTokens = []; // {name, light:{...}, dark:{...}} +const dimenTokens = []; // {name, value} px → pt/dp +const otherSkipped = []; + +const camel = (s) => s.replace(/-(\w)/g, (_, c) => c.toUpperCase()); + +for (const [name, raw] of Object.entries(light)) { + const lv = resolveVar(raw, light); + const dvRaw = dark[name]; + const dv = dvRaw ? resolveVar(dvRaw, dark) : lv; + const lc = parseColor(lv); + if (lc) { + colorTokens.push({ name: camel(name), light: lc, dark: parseColor(dv) ?? lc }); + continue; + } + const px = lv.match(/^(-?[\d.]+)px$/); + if (px) { + dimenTokens.push({ name: camel(name), value: parseFloat(px[1]) }); + continue; + } + otherSkipped.push(name); +} + +colorTokens.sort((a, b) => a.name.localeCompare(b.name)); +dimenTokens.sort((a, b) => a.name.localeCompare(b.name)); + +// ─── 生成 Swift ─────────────────────────────────────────────────────────────── + +const HEADER = `// 自动生成 — 请勿手改。来源 design/tokens/*.css +// 重新生成:node design-pipeline/export-tokens.mjs +`; + +function swift() { + const colors = colorTokens.map((t) => { + const l = t.light, d = t.dark; + return ` public static let ${t.name} = dynamic( + light: Color(.sRGB, red: ${l.r.toFixed(4)}, green: ${l.g.toFixed(4)}, blue: ${l.b.toFixed(4)}, opacity: ${l.a.toFixed(2)}), + dark: Color(.sRGB, red: ${d.r.toFixed(4)}, green: ${d.g.toFixed(4)}, blue: ${d.b.toFixed(4)}, opacity: ${d.a.toFixed(2)}))`; + }).join('\n'); + const dims = dimenTokens.map((t) => ` public static let ${t.name}: CGFloat = ${t.value}`).join('\n'); + return `${HEADER} +import SwiftUI + +public enum DuduTheme { + /// light/dark 动态色(跟随系统外观) + static func dynamic(light: Color, dark: Color) -> Color { + Color(UIColor { trait in + trait.userInterfaceStyle == .dark ? UIColor(dark) : UIColor(light) + }) + } + + // MARK: - Colors +${colors} + + // MARK: - Dimensions (pt) +${dims} +} +`; +} + +// ─── 生成 Kotlin ───────────────────────────────────────────────────────────── + +function kotlin() { + const group = (scope) => colorTokens.map((t) => { + const c = scope === 'light' ? t.light : t.dark; + const argb = `0x${hex2(c.a)}${hex2(c.r)}${hex2(c.g)}${hex2(c.b)}`; + return ` val ${t.name} = Color(${argb})`; + }).join('\n'); + const dims = dimenTokens.map((t) => + Number.isInteger(t.value) ? ` val ${t.name} = ${t.value}.dp` : ` val ${t.name} = ${t.value}f.dp` + ).join('\n'); + return `${HEADER} +package app.dudu.design + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp + +object DuduTheme { + object Light { +${group('light')} + } + + object Dark { +${group('dark')} + } + + // Dimensions (dp) +${dims} +} +`; +} + +// ─── 输出 / 校验 ───────────────────────────────────────────────────────────── + +const outputs = [ + [OUT_IOS, swift()], + [OUT_ANDROID, kotlin()], +]; + +const check = process.argv.includes('--check'); +let dirty = false; +for (const [path, content] of outputs) { + if (check) { + const cur = existsSync(path) ? readFileSync(path, 'utf8') : ''; + if (cur !== content) { + console.error(`❌ 过期:${path}(请重新运行导出脚本并提交)`); + dirty = true; + } + } else { + mkdirSync(dirname(path), { recursive: true }); + writeFileSync(path, content, 'utf8'); + console.log(`✅ ${path}`); + } +} +if (check && dirty) process.exit(1); +if (check) console.log('✅ 令牌产物与源一致'); +console.log(`颜色 ${colorTokens.length} · 尺寸 ${dimenTokens.length} · 跳过(非色非px) ${otherSkipped.length}: ${otherSkipped.join(', ')}`); diff --git a/design-pipeline/generated/android/DuduTheme.kt b/design-pipeline/generated/android/DuduTheme.kt new file mode 100644 index 0000000..0039d6d --- /dev/null +++ b/design-pipeline/generated/android/DuduTheme.kt @@ -0,0 +1,156 @@ +// 自动生成 — 请勿手改。来源 design/tokens/*.css +// 重新生成:node design-pipeline/export-tokens.mjs + +package app.dudu.design + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp + +object DuduTheme { + object Light { + val accent = Color(0xFF4F6EF7) + val accentHover = Color(0xFF3D58DB) + val accentPress = Color(0xFF2F44B3) + val accentSoft = Color(0xFFEEF1FE) + val accentSoft2 = Color(0xFFDFE5FD) + val accentText = Color(0xFF3D58DB) + val amber100 = Color(0xFFFCEFD7) + val amber500 = Color(0xFFE8890C) + val bgApp = Color(0xFFF6F7FA) + val blue100 = Color(0xFFDFE5FD) + val blue200 = Color(0xFFC4CFFB) + val blue300 = Color(0xFF9FB1F9) + val blue400 = Color(0xFF7590F8) + val blue50 = Color(0xFFEEF1FE) + val blue500 = Color(0xFF4F6EF7) + val blue600 = Color(0xFF3D58DB) + val blue700 = Color(0xFF2F44B3) + val blue800 = Color(0xFF25368C) + val blue900 = Color(0xFF1E2B6B) + val border1 = Color(0xFFE4E6EB) + val border2 = Color(0xFFD2D5DD) + val danger = Color(0xFFDC2626) + val dangerSoft = Color(0xFFFEE2E2) + val gray0 = Color(0xFFFFFFFF) + val gray100 = Color(0xFFF0F1F4) + val gray200 = Color(0xFFE4E6EB) + val gray25 = Color(0xFFFCFCFD) + val gray300 = Color(0xFFD2D5DD) + val gray400 = Color(0xFFA6ABB8) + val gray50 = Color(0xFFF6F7FA) + val gray500 = Color(0xFF7A8090) + val gray600 = Color(0xFF5A6072) + val gray700 = Color(0xFF434957) + val gray800 = Color(0xFF2B2F3A) + val gray900 = Color(0xFF1B1E26) + val gray950 = Color(0xFF11131A) + val green100 = Color(0xFFDCFCE7) + val green500 = Color(0xFF16A34A) + val overlayBg = Color(0xF2181A22) + val overlayText = Color(0xFFF2F4FA) + val overlayText2 = Color(0xFF9AA3BD) + val overlayText3 = Color(0xFF646E8C) + val positive = Color(0xFF16A34A) + val positiveSoft = Color(0xFFDCFCE7) + val red100 = Color(0xFFFEE2E2) + val red500 = Color(0xFFDC2626) + val surface2 = Color(0xFFF0F1F4) + val surface3 = Color(0xFFE4E6EB) + val surfaceCard = Color(0xFFFFFFFF) + val text1 = Color(0xFF1B1E26) + val text2 = Color(0xFF5A6072) + val text3 = Color(0xFFA6ABB8) + val textOnAccent = Color(0xFFFFFFFF) + val warning = Color(0xFFE8890C) + val warningSoft = Color(0xFFFCEFD7) + } + + object Dark { + val accent = Color(0xFF5C77E8) + val accentHover = Color(0xFF6E89F9) + val accentPress = Color(0xFF4A63D6) + val accentSoft = Color(0x295C77E8) + val accentSoft2 = Color(0x425C77E8) + val accentText = Color(0xFF7D95F7) + val amber100 = Color(0xFFFCEFD7) + val amber500 = Color(0xFFE8890C) + val bgApp = Color(0xFF0F1116) + val blue100 = Color(0xFFDFE5FD) + val blue200 = Color(0xFFC4CFFB) + val blue300 = Color(0xFF9FB1F9) + val blue400 = Color(0xFF7590F8) + val blue50 = Color(0xFFEEF1FE) + val blue500 = Color(0xFF4F6EF7) + val blue600 = Color(0xFF3D58DB) + val blue700 = Color(0xFF2F44B3) + val blue800 = Color(0xFF25368C) + val blue900 = Color(0xFF1E2B6B) + val border1 = Color(0xFF272C38) + val border2 = Color(0xFF343A49) + val danger = Color(0xFFF05B5B) + val dangerSoft = Color(0x29F05B5B) + val gray0 = Color(0xFFFFFFFF) + val gray100 = Color(0xFFF0F1F4) + val gray200 = Color(0xFFE4E6EB) + val gray25 = Color(0xFFFCFCFD) + val gray300 = Color(0xFFD2D5DD) + val gray400 = Color(0xFFA6ABB8) + val gray50 = Color(0xFFF6F7FA) + val gray500 = Color(0xFF7A8090) + val gray600 = Color(0xFF5A6072) + val gray700 = Color(0xFF434957) + val gray800 = Color(0xFF2B2F3A) + val gray900 = Color(0xFF1B1E26) + val gray950 = Color(0xFF11131A) + val green100 = Color(0xFFDCFCE7) + val green500 = Color(0xFF16A34A) + val overlayBg = Color(0xF51E212B) + val overlayText = Color(0xFFF2F4FA) + val overlayText2 = Color(0xFF9AA3BD) + val overlayText3 = Color(0xFF646E8C) + val positive = Color(0xFF34C46A) + val positiveSoft = Color(0x2934C46A) + val red100 = Color(0xFFFEE2E2) + val red500 = Color(0xFFDC2626) + val surface2 = Color(0xFF1E222C) + val surface3 = Color(0xFF262B37) + val surfaceCard = Color(0xFF171A22) + val text1 = Color(0xFFECEEF4) + val text2 = Color(0xFF9AA1B2) + val text3 = Color(0xFF5F6678) + val textOnAccent = Color(0xFFFFFFFF) + val warning = Color(0xFFF2A33C) + val warningSoft = Color(0x29F2A33C) + } + + // Dimensions (dp) + val controlLg = 44.dp + val controlMd = 36.dp + val controlSm = 28.dp + val controlXl = 56.dp + val radiusFull = 999.dp + val radiusLg = 14.dp + val radiusMd = 10.dp + val radiusSm = 6.dp + val radiusXl = 20.dp + val radiusXs = 4.dp + val space1 = 4.dp + val space10 = 40.dp + val space12 = 48.dp + val space16 = 64.dp + val space2 = 8.dp + val space3 = 12.dp + val space4 = 16.dp + val space5 = 20.dp + val space6 = 24.dp + val space8 = 32.dp + val text2xl = 24.dp + val text3xl = 32.dp + val text4xl = 44.dp + val textBase = 14.dp + val textLg = 17.dp + val textMd = 15.dp + val textSm = 13.dp + val textXl = 20.dp + val textXs = 12.dp +} diff --git a/design-pipeline/generated/ios/DuduTheme.swift b/design-pipeline/generated/ios/DuduTheme.swift new file mode 100644 index 0000000..78cfab5 --- /dev/null +++ b/design-pipeline/generated/ios/DuduTheme.swift @@ -0,0 +1,211 @@ +// 自动生成 — 请勿手改。来源 design/tokens/*.css +// 重新生成:node design-pipeline/export-tokens.mjs + +import SwiftUI + +public enum DuduTheme { + /// light/dark 动态色(跟随系统外观) + static func dynamic(light: Color, dark: Color) -> Color { + Color(UIColor { trait in + trait.userInterfaceStyle == .dark ? UIColor(dark) : UIColor(light) + }) + } + + // MARK: - Colors + public static let accent = dynamic( + light: Color(.sRGB, red: 0.3098, green: 0.4314, blue: 0.9686, opacity: 1.00), + dark: Color(.sRGB, red: 0.3608, green: 0.4667, blue: 0.9098, opacity: 1.00)) + public static let accentHover = dynamic( + light: Color(.sRGB, red: 0.2392, green: 0.3451, blue: 0.8588, opacity: 1.00), + dark: Color(.sRGB, red: 0.4314, green: 0.5373, blue: 0.9765, opacity: 1.00)) + public static let accentPress = dynamic( + light: Color(.sRGB, red: 0.1843, green: 0.2667, blue: 0.7020, opacity: 1.00), + dark: Color(.sRGB, red: 0.2902, green: 0.3882, blue: 0.8392, opacity: 1.00)) + public static let accentSoft = dynamic( + light: Color(.sRGB, red: 0.9333, green: 0.9451, blue: 0.9961, opacity: 1.00), + dark: Color(.sRGB, red: 0.3608, green: 0.4667, blue: 0.9098, opacity: 0.16)) + public static let accentSoft2 = dynamic( + light: Color(.sRGB, red: 0.8745, green: 0.8980, blue: 0.9922, opacity: 1.00), + dark: Color(.sRGB, red: 0.3608, green: 0.4667, blue: 0.9098, opacity: 0.26)) + public static let accentText = dynamic( + light: Color(.sRGB, red: 0.2392, green: 0.3451, blue: 0.8588, opacity: 1.00), + dark: Color(.sRGB, red: 0.4902, green: 0.5843, blue: 0.9686, opacity: 1.00)) + public static let amber100 = dynamic( + light: Color(.sRGB, red: 0.9882, green: 0.9373, blue: 0.8431, opacity: 1.00), + dark: Color(.sRGB, red: 0.9882, green: 0.9373, blue: 0.8431, opacity: 1.00)) + public static let amber500 = dynamic( + light: Color(.sRGB, red: 0.9098, green: 0.5373, blue: 0.0471, opacity: 1.00), + dark: Color(.sRGB, red: 0.9098, green: 0.5373, blue: 0.0471, opacity: 1.00)) + public static let bgApp = dynamic( + light: Color(.sRGB, red: 0.9647, green: 0.9686, blue: 0.9804, opacity: 1.00), + dark: Color(.sRGB, red: 0.0588, green: 0.0667, blue: 0.0863, opacity: 1.00)) + public static let blue100 = dynamic( + light: Color(.sRGB, red: 0.8745, green: 0.8980, blue: 0.9922, opacity: 1.00), + dark: Color(.sRGB, red: 0.8745, green: 0.8980, blue: 0.9922, opacity: 1.00)) + public static let blue200 = dynamic( + light: Color(.sRGB, red: 0.7686, green: 0.8118, blue: 0.9843, opacity: 1.00), + dark: Color(.sRGB, red: 0.7686, green: 0.8118, blue: 0.9843, opacity: 1.00)) + public static let blue300 = dynamic( + light: Color(.sRGB, red: 0.6235, green: 0.6941, blue: 0.9765, opacity: 1.00), + dark: Color(.sRGB, red: 0.6235, green: 0.6941, blue: 0.9765, opacity: 1.00)) + public static let blue400 = dynamic( + light: Color(.sRGB, red: 0.4588, green: 0.5647, blue: 0.9725, opacity: 1.00), + dark: Color(.sRGB, red: 0.4588, green: 0.5647, blue: 0.9725, opacity: 1.00)) + public static let blue50 = dynamic( + light: Color(.sRGB, red: 0.9333, green: 0.9451, blue: 0.9961, opacity: 1.00), + dark: Color(.sRGB, red: 0.9333, green: 0.9451, blue: 0.9961, opacity: 1.00)) + public static let blue500 = dynamic( + light: Color(.sRGB, red: 0.3098, green: 0.4314, blue: 0.9686, opacity: 1.00), + dark: Color(.sRGB, red: 0.3098, green: 0.4314, blue: 0.9686, opacity: 1.00)) + public static let blue600 = dynamic( + light: Color(.sRGB, red: 0.2392, green: 0.3451, blue: 0.8588, opacity: 1.00), + dark: Color(.sRGB, red: 0.2392, green: 0.3451, blue: 0.8588, opacity: 1.00)) + public static let blue700 = dynamic( + light: Color(.sRGB, red: 0.1843, green: 0.2667, blue: 0.7020, opacity: 1.00), + dark: Color(.sRGB, red: 0.1843, green: 0.2667, blue: 0.7020, opacity: 1.00)) + public static let blue800 = dynamic( + light: Color(.sRGB, red: 0.1451, green: 0.2118, blue: 0.5490, opacity: 1.00), + dark: Color(.sRGB, red: 0.1451, green: 0.2118, blue: 0.5490, opacity: 1.00)) + public static let blue900 = dynamic( + light: Color(.sRGB, red: 0.1176, green: 0.1686, blue: 0.4196, opacity: 1.00), + dark: Color(.sRGB, red: 0.1176, green: 0.1686, blue: 0.4196, opacity: 1.00)) + public static let border1 = dynamic( + light: Color(.sRGB, red: 0.8941, green: 0.9020, blue: 0.9216, opacity: 1.00), + dark: Color(.sRGB, red: 0.1529, green: 0.1725, blue: 0.2196, opacity: 1.00)) + public static let border2 = dynamic( + light: Color(.sRGB, red: 0.8235, green: 0.8353, blue: 0.8667, opacity: 1.00), + dark: Color(.sRGB, red: 0.2039, green: 0.2275, blue: 0.2863, opacity: 1.00)) + public static let danger = dynamic( + light: Color(.sRGB, red: 0.8627, green: 0.1490, blue: 0.1490, opacity: 1.00), + dark: Color(.sRGB, red: 0.9412, green: 0.3569, blue: 0.3569, opacity: 1.00)) + public static let dangerSoft = dynamic( + light: Color(.sRGB, red: 0.9961, green: 0.8863, blue: 0.8863, opacity: 1.00), + dark: Color(.sRGB, red: 0.9412, green: 0.3569, blue: 0.3569, opacity: 0.16)) + public static let gray0 = dynamic( + light: Color(.sRGB, red: 1.0000, green: 1.0000, blue: 1.0000, opacity: 1.00), + dark: Color(.sRGB, red: 1.0000, green: 1.0000, blue: 1.0000, opacity: 1.00)) + public static let gray100 = dynamic( + light: Color(.sRGB, red: 0.9412, green: 0.9451, blue: 0.9569, opacity: 1.00), + dark: Color(.sRGB, red: 0.9412, green: 0.9451, blue: 0.9569, opacity: 1.00)) + public static let gray200 = dynamic( + light: Color(.sRGB, red: 0.8941, green: 0.9020, blue: 0.9216, opacity: 1.00), + dark: Color(.sRGB, red: 0.8941, green: 0.9020, blue: 0.9216, opacity: 1.00)) + public static let gray25 = dynamic( + light: Color(.sRGB, red: 0.9882, green: 0.9882, blue: 0.9922, opacity: 1.00), + dark: Color(.sRGB, red: 0.9882, green: 0.9882, blue: 0.9922, opacity: 1.00)) + public static let gray300 = dynamic( + light: Color(.sRGB, red: 0.8235, green: 0.8353, blue: 0.8667, opacity: 1.00), + dark: Color(.sRGB, red: 0.8235, green: 0.8353, blue: 0.8667, opacity: 1.00)) + public static let gray400 = dynamic( + light: Color(.sRGB, red: 0.6510, green: 0.6706, blue: 0.7216, opacity: 1.00), + dark: Color(.sRGB, red: 0.6510, green: 0.6706, blue: 0.7216, opacity: 1.00)) + public static let gray50 = dynamic( + light: Color(.sRGB, red: 0.9647, green: 0.9686, blue: 0.9804, opacity: 1.00), + dark: Color(.sRGB, red: 0.9647, green: 0.9686, blue: 0.9804, opacity: 1.00)) + public static let gray500 = dynamic( + light: Color(.sRGB, red: 0.4784, green: 0.5020, blue: 0.5647, opacity: 1.00), + dark: Color(.sRGB, red: 0.4784, green: 0.5020, blue: 0.5647, opacity: 1.00)) + public static let gray600 = dynamic( + light: Color(.sRGB, red: 0.3529, green: 0.3765, blue: 0.4471, opacity: 1.00), + dark: Color(.sRGB, red: 0.3529, green: 0.3765, blue: 0.4471, opacity: 1.00)) + public static let gray700 = dynamic( + light: Color(.sRGB, red: 0.2627, green: 0.2863, blue: 0.3412, opacity: 1.00), + dark: Color(.sRGB, red: 0.2627, green: 0.2863, blue: 0.3412, opacity: 1.00)) + public static let gray800 = dynamic( + light: Color(.sRGB, red: 0.1686, green: 0.1843, blue: 0.2275, opacity: 1.00), + dark: Color(.sRGB, red: 0.1686, green: 0.1843, blue: 0.2275, opacity: 1.00)) + public static let gray900 = dynamic( + light: Color(.sRGB, red: 0.1059, green: 0.1176, blue: 0.1490, opacity: 1.00), + dark: Color(.sRGB, red: 0.1059, green: 0.1176, blue: 0.1490, opacity: 1.00)) + public static let gray950 = dynamic( + light: Color(.sRGB, red: 0.0667, green: 0.0745, blue: 0.1020, opacity: 1.00), + dark: Color(.sRGB, red: 0.0667, green: 0.0745, blue: 0.1020, opacity: 1.00)) + public static let green100 = dynamic( + light: Color(.sRGB, red: 0.8627, green: 0.9882, blue: 0.9059, opacity: 1.00), + dark: Color(.sRGB, red: 0.8627, green: 0.9882, blue: 0.9059, opacity: 1.00)) + public static let green500 = dynamic( + light: Color(.sRGB, red: 0.0863, green: 0.6392, blue: 0.2902, opacity: 1.00), + dark: Color(.sRGB, red: 0.0863, green: 0.6392, blue: 0.2902, opacity: 1.00)) + public static let overlayBg = dynamic( + light: Color(.sRGB, red: 0.0941, green: 0.1020, blue: 0.1333, opacity: 0.95), + dark: Color(.sRGB, red: 0.1176, green: 0.1294, blue: 0.1686, opacity: 0.96)) + public static let overlayText = dynamic( + light: Color(.sRGB, red: 0.9490, green: 0.9569, blue: 0.9804, opacity: 1.00), + dark: Color(.sRGB, red: 0.9490, green: 0.9569, blue: 0.9804, opacity: 1.00)) + public static let overlayText2 = dynamic( + light: Color(.sRGB, red: 0.6039, green: 0.6392, blue: 0.7412, opacity: 1.00), + dark: Color(.sRGB, red: 0.6039, green: 0.6392, blue: 0.7412, opacity: 1.00)) + public static let overlayText3 = dynamic( + light: Color(.sRGB, red: 0.3922, green: 0.4314, blue: 0.5490, opacity: 1.00), + dark: Color(.sRGB, red: 0.3922, green: 0.4314, blue: 0.5490, opacity: 1.00)) + public static let positive = dynamic( + light: Color(.sRGB, red: 0.0863, green: 0.6392, blue: 0.2902, opacity: 1.00), + dark: Color(.sRGB, red: 0.2039, green: 0.7686, blue: 0.4157, opacity: 1.00)) + public static let positiveSoft = dynamic( + light: Color(.sRGB, red: 0.8627, green: 0.9882, blue: 0.9059, opacity: 1.00), + dark: Color(.sRGB, red: 0.2039, green: 0.7686, blue: 0.4157, opacity: 0.16)) + public static let red100 = dynamic( + light: Color(.sRGB, red: 0.9961, green: 0.8863, blue: 0.8863, opacity: 1.00), + dark: Color(.sRGB, red: 0.9961, green: 0.8863, blue: 0.8863, opacity: 1.00)) + public static let red500 = dynamic( + light: Color(.sRGB, red: 0.8627, green: 0.1490, blue: 0.1490, opacity: 1.00), + dark: Color(.sRGB, red: 0.8627, green: 0.1490, blue: 0.1490, opacity: 1.00)) + public static let surface2 = dynamic( + light: Color(.sRGB, red: 0.9412, green: 0.9451, blue: 0.9569, opacity: 1.00), + dark: Color(.sRGB, red: 0.1176, green: 0.1333, blue: 0.1725, opacity: 1.00)) + public static let surface3 = dynamic( + light: Color(.sRGB, red: 0.8941, green: 0.9020, blue: 0.9216, opacity: 1.00), + dark: Color(.sRGB, red: 0.1490, green: 0.1686, blue: 0.2157, opacity: 1.00)) + public static let surfaceCard = dynamic( + light: Color(.sRGB, red: 1.0000, green: 1.0000, blue: 1.0000, opacity: 1.00), + dark: Color(.sRGB, red: 0.0902, green: 0.1020, blue: 0.1333, opacity: 1.00)) + public static let text1 = dynamic( + light: Color(.sRGB, red: 0.1059, green: 0.1176, blue: 0.1490, opacity: 1.00), + dark: Color(.sRGB, red: 0.9255, green: 0.9333, blue: 0.9569, opacity: 1.00)) + public static let text2 = dynamic( + light: Color(.sRGB, red: 0.3529, green: 0.3765, blue: 0.4471, opacity: 1.00), + dark: Color(.sRGB, red: 0.6039, green: 0.6314, blue: 0.6980, opacity: 1.00)) + public static let text3 = dynamic( + light: Color(.sRGB, red: 0.6510, green: 0.6706, blue: 0.7216, opacity: 1.00), + dark: Color(.sRGB, red: 0.3725, green: 0.4000, blue: 0.4706, opacity: 1.00)) + public static let textOnAccent = dynamic( + light: Color(.sRGB, red: 1.0000, green: 1.0000, blue: 1.0000, opacity: 1.00), + dark: Color(.sRGB, red: 1.0000, green: 1.0000, blue: 1.0000, opacity: 1.00)) + public static let warning = dynamic( + light: Color(.sRGB, red: 0.9098, green: 0.5373, blue: 0.0471, opacity: 1.00), + dark: Color(.sRGB, red: 0.9490, green: 0.6392, blue: 0.2353, opacity: 1.00)) + public static let warningSoft = dynamic( + light: Color(.sRGB, red: 0.9882, green: 0.9373, blue: 0.8431, opacity: 1.00), + dark: Color(.sRGB, red: 0.9490, green: 0.6392, blue: 0.2353, opacity: 0.16)) + + // MARK: - Dimensions (pt) + public static let controlLg: CGFloat = 44 + public static let controlMd: CGFloat = 36 + public static let controlSm: CGFloat = 28 + public static let controlXl: CGFloat = 56 + public static let radiusFull: CGFloat = 999 + public static let radiusLg: CGFloat = 14 + public static let radiusMd: CGFloat = 10 + public static let radiusSm: CGFloat = 6 + public static let radiusXl: CGFloat = 20 + public static let radiusXs: CGFloat = 4 + public static let space1: CGFloat = 4 + public static let space10: CGFloat = 40 + public static let space12: CGFloat = 48 + public static let space16: CGFloat = 64 + public static let space2: CGFloat = 8 + public static let space3: CGFloat = 12 + public static let space4: CGFloat = 16 + public static let space5: CGFloat = 20 + public static let space6: CGFloat = 24 + public static let space8: CGFloat = 32 + public static let text2xl: CGFloat = 24 + public static let text3xl: CGFloat = 32 + public static let text4xl: CGFloat = 44 + public static let textBase: CGFloat = 14 + public static let textLg: CGFloat = 17 + public static let textMd: CGFloat = 15 + public static let textSm: CGFloat = 13 + public static let textXl: CGFloat = 20 + public static let textXs: CGFloat = 12 +} diff --git a/design/SKILL.md b/design/SKILL.md new file mode 100644 index 0000000..f39fc1f --- /dev/null +++ b/design/SKILL.md @@ -0,0 +1,15 @@ +--- +name: dudu-design +description: Use this skill to generate well-branded interfaces and assets for dudu (嘟嘟语音输入法), either for production or throwaway prototypes/mocks/etc. Contains essential design guidelines, colors, type, fonts, assets, and UI kit components for prototyping. +user-invocable: true +--- + +Read the README.md file within this skill, and explore the other available files. + +Key facts: dudu is a paid voice input method (语音输入法) for Mac/Windows/iOS/Android — push-to-talk dictation with streaming recognition. Minimal aesthetic, light/dark themes via `data-theme="dark"`, single accent blue, Lucide line icons, circle-mouth line logo. Link `styles.css` for all tokens; reusable React components live in `components/` (Button, Switch, MicBar, RecognitionOverlay, …); full-screen interactive recreations in `ui_kits/desktop` and `ui_kits/mobile`. + +If creating visual artifacts (slides, mocks, throwaway prototypes, etc), copy assets out and create static HTML files for the user to view. If working on production code, you can copy assets and read the rules here to become an expert in designing with this brand. + +If the user invokes this skill without any other guidance, ask them what they want to build or design, ask some questions, and act as an expert designer who outputs HTML artifacts _or_ production code, depending on the need. + +Hard rules: 一屏一个蓝色主按钮;无渐变;无 emoji;浮层永远深色玻璃;文案短、用"你"、无句号。 diff --git a/design/_adherence.oxlintrc.json b/design/_adherence.oxlintrc.json new file mode 100644 index 0000000..da716d0 --- /dev/null +++ b/design/_adherence.oxlintrc.json @@ -0,0 +1,387 @@ +{ + "plugins": [ + "react", + "import" + ], + "rules": { + "react/forbid-elements": [ + "warn", + { + "forbid": [] + } + ], + "no-restricted-imports": [ + "warn", + { + "patterns": [ + { + "group": [ + "components/core/**", + "components/forms/**", + "components/voice/**", + "ui_kits/desktop/**", + "ui_kits/mobile/**" + ], + "message": "Import design-system components from 'index.js', not component internals." + } + ] + } + ], + "no-restricted-syntax": [ + "warn", + { + "selector": "Literal[value=/#[0-9a-fA-F]{3,8}\\b/]", + "message": "Raw hex color — use a design-system color token via var()." + }, + { + "selector": "Literal[value=/\\b\\d+px\\b/]", + "message": "Raw px value — use a design-system spacing token via var()." + }, + { + "selector": "Literal[value=/font-family\\s*:\\s*(?!['\\\"]?(?:Outfit))/i]", + "message": "Font not provided by the design system. Available: Outfit." + }, + { + "selector": "JSXOpeningElement[name.name='Badge'] > JSXAttribute > JSXIdentifier[name!=/^(?:tone|children|key|ref|className|style|children)$/]", + "message": " doesn't accept that prop. Declared props: tone, children." + }, + { + "selector": "JSXOpeningElement[name.name='Badge'] > JSXAttribute[name.name='tone'] > Literal[value!=/^(?:neutral|blue|green|orange|red)$/]", + "message": " tone must be one of 'neutral' | 'blue' | 'green' | 'orange' | 'red'." + }, + { + "selector": "JSXOpeningElement[name.name='Button'] > JSXAttribute > JSXIdentifier[name!=/^(?:variant|size|block|disabled|onClick|children|key|ref|className|style|children)$/]", + "message": "; +} diff --git a/design/components/core/Button.prompt.md b/design/components/core/Button.prompt.md new file mode 100644 index 0000000..1737dce --- /dev/null +++ b/design/components/core/Button.prompt.md @@ -0,0 +1,10 @@ +Primary action control. Use `primary` for the single main action per view, `secondary` for the rest, `ghost` for inline/quiet actions, `danger` for destructive ones. + +```jsx +const { Button } = window.DuduDesignSystem_2e0172; + + + +``` + +Variants: primary / secondary (bordered) / ghost (text+tint hover) / danger. Sizes sm 28px · md 36px · lg 44px (mobile). `block` fills the row. Never put two primary buttons side by side. diff --git a/design/components/core/Card.d.ts b/design/components/core/Card.d.ts new file mode 100644 index 0000000..d563ddc --- /dev/null +++ b/design/components/core/Card.d.ts @@ -0,0 +1,8 @@ +/** 卡片 — 白底(dark: surface-card)+ 1px 边框 + 极轻投影的内容容器。 */ +export interface CardProps { + /** 内边距 @default 'var(--space-5)' */ + padding?: string | number; + style?: any; + children?: any; +} +export declare function Card(props: CardProps): any; diff --git a/design/components/core/Card.jsx b/design/components/core/Card.jsx new file mode 100644 index 0000000..d61da0c --- /dev/null +++ b/design/components/core/Card.jsx @@ -0,0 +1,17 @@ +const ddCardCss = ` +.dd-card { + background: var(--surface-card); border: 1px solid var(--border-1); + border-radius: var(--radius-md); box-shadow: var(--shadow-card); +} +`; + +if (typeof document !== 'undefined' && !document.getElementById('dd-card-css')) { + const s = document.createElement('style'); + s.id = 'dd-card-css'; + s.textContent = ddCardCss; + document.head.appendChild(s); +} + +export function Card({ padding = 'var(--space-5)', style, children, ...rest }) { + return
{children}
; +} diff --git a/design/components/core/Card.prompt.md b/design/components/core/Card.prompt.md new file mode 100644 index 0000000..f5258ce --- /dev/null +++ b/design/components/core/Card.prompt.md @@ -0,0 +1,8 @@ +Surface container: border + faint shadow, 10px radius. The default grouping element on both desktop and mobile. + +```jsx +const { Card } = window.DuduDesignSystem_2e0172; + +``` + +Don't nest cards; use `--surface-2` wells inside a card instead. diff --git a/design/components/core/IconButton.d.ts b/design/components/core/IconButton.d.ts new file mode 100644 index 0000000..24e5e91 --- /dev/null +++ b/design/components/core/IconButton.d.ts @@ -0,0 +1,12 @@ +/** 图标按钮 — 无边框方形热区,包裹一个线性 SVG 图标。 */ +export interface IconButtonProps { + /** 方形边长 px @default 32 */ + size?: number; + /** 无障碍标签(必填语义) */ + label?: string; + disabled?: boolean; + onClick?: (e: any) => void; + /** 内联 SVG 图标(Lucide 风格,stroke=currentColor) */ + children?: any; +} +export declare function IconButton(props: IconButtonProps): any; diff --git a/design/components/core/IconButton.jsx b/design/components/core/IconButton.jsx new file mode 100644 index 0000000..5d4284d --- /dev/null +++ b/design/components/core/IconButton.jsx @@ -0,0 +1,28 @@ +const ddIconBtnCss = ` +.dd-iconbtn { + display: inline-flex; align-items: center; justify-content: center; + border: none; background: transparent; color: var(--text-2); + border-radius: var(--radius-sm); cursor: pointer; + transition: background var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out); +} +.dd-iconbtn:hover { background: var(--surface-2); color: var(--text-1); } +.dd-iconbtn:active { background: var(--surface-3); } +.dd-iconbtn:focus-visible { outline: none; box-shadow: var(--focus-ring); } +.dd-iconbtn[disabled] { opacity: 0.45; pointer-events: none; } +`; + +if (typeof document !== 'undefined' && !document.getElementById('dd-iconbtn-css')) { + const s = document.createElement('style'); + s.id = 'dd-iconbtn-css'; + s.textContent = ddIconBtnCss; + document.head.appendChild(s); +} + +export function IconButton({ size = 32, label, disabled = false, children, ...rest }) { + return ( + + ); +} diff --git a/design/components/core/IconButton.prompt.md b/design/components/core/IconButton.prompt.md new file mode 100644 index 0000000..341d4d0 --- /dev/null +++ b/design/components/core/IconButton.prompt.md @@ -0,0 +1,10 @@ +Square borderless hit-area for a single line icon (settings gear, close, theme toggle). + +```jsx +const { IconButton } = window.DuduDesignSystem_2e0172; + + + +``` + +Icon inherits `currentColor` (text-2, darkens on hover). Keep icons 16–20px inside a 28–36px button. diff --git a/design/components/core/Kbd.d.ts b/design/components/core/Kbd.d.ts new file mode 100644 index 0000000..1eaaf9a --- /dev/null +++ b/design/components/core/Kbd.d.ts @@ -0,0 +1,7 @@ +/** 键帽 — 展示快捷键的单个按键。HotkeyCombo 排列一组键帽。 */ +export interface KbdProps { + /** 键名:⌘ ⇧ ⌥ ⌃ Space Enter 等 */ + children?: any; +} +export declare function Kbd(props: KbdProps): any; +export declare function HotkeyCombo(props: { keys: string[]; gap?: number }): any; diff --git a/design/components/core/Kbd.jsx b/design/components/core/Kbd.jsx new file mode 100644 index 0000000..847366e --- /dev/null +++ b/design/components/core/Kbd.jsx @@ -0,0 +1,28 @@ +const ddKbdCss = ` +.dd-kbd { + display: inline-block; background: var(--surface-2); + border: 1px solid var(--border-2); border-bottom-width: 2px; + border-radius: var(--radius-xs); padding: 1px 7px; + font-family: var(--font-mono); font-size: var(--text-xs); color: var(--text-1); + line-height: 1.5; +} +`; + +if (typeof document !== 'undefined' && !document.getElementById('dd-kbd-css')) { + const s = document.createElement('style'); + s.id = 'dd-kbd-css'; + s.textContent = ddKbdCss; + document.head.appendChild(s); +} + +export function Kbd({ children, ...rest }) { + return {children}; +} + +export function HotkeyCombo({ keys = [], gap = 4 }) { + return ( + + {keys.map((k, i) => {k})} + + ); +} diff --git a/design/components/core/Kbd.prompt.md b/design/components/core/Kbd.prompt.md new file mode 100644 index 0000000..e700eaf --- /dev/null +++ b/design/components/core/Kbd.prompt.md @@ -0,0 +1,9 @@ +Keycap chip for shortcuts — core to dudu's push-to-talk identity. + +```jsx +const { Kbd, HotkeyCombo } = window.DuduDesignSystem_2e0172; + // 按住说话快捷键 +Enter +``` + +Use unicode modifier symbols (⌘ ⇧ ⌥ ⌃), not words. diff --git a/design/components/core/ProgressBar.d.ts b/design/components/core/ProgressBar.d.ts new file mode 100644 index 0000000..e764e30 --- /dev/null +++ b/design/components/core/ProgressBar.d.ts @@ -0,0 +1,11 @@ +/** 用量进度条 — 本月识别分钟数等配额展示;接近上限自动转橙色。 */ +export interface ProgressBarProps { + value?: number; + max?: number; + /** 条高 px @default 6 */ + height?: number; + /** 超过该比例转为警告色 @default 0.9 */ + warnAt?: number; + style?: any; +} +export declare function ProgressBar(props: ProgressBarProps): any; diff --git a/design/components/core/ProgressBar.jsx b/design/components/core/ProgressBar.jsx new file mode 100644 index 0000000..4baac12 --- /dev/null +++ b/design/components/core/ProgressBar.jsx @@ -0,0 +1,24 @@ +const ddProgressCss = ` +.dd-progress { background: var(--surface-3); border-radius: var(--radius-full); overflow: hidden; } +.dd-progress i { display: block; height: 100%; border-radius: var(--radius-full); + background: var(--accent); transition: width var(--dur-slow) var(--ease-out); } +.dd-progress--warning i { background: var(--warning); } +`; + +if (typeof document !== 'undefined' && !document.getElementById('dd-progress-css')) { + const s = document.createElement('style'); + s.id = 'dd-progress-css'; + s.textContent = ddProgressCss; + document.head.appendChild(s); +} + +export function ProgressBar({ value = 0, max = 100, height = 6, warnAt = 0.9, style, ...rest }) { + const ratio = Math.min(1, max > 0 ? value / max : 0); + const warning = ratio >= warnAt; + return ( +
+ +
+ ); +} diff --git a/design/components/core/ProgressBar.prompt.md b/design/components/core/ProgressBar.prompt.md new file mode 100644 index 0000000..eb6ba4f --- /dev/null +++ b/design/components/core/ProgressBar.prompt.md @@ -0,0 +1,8 @@ +Quota/usage meter (e.g. 128 / 600 分钟). Turns warning-orange past `warnAt` (default 90%). + +```jsx +const { ProgressBar } = window.DuduDesignSystem_2e0172; + +``` + +Pair with a `13px text-2` label row above: 「本月已用 128 / 600 分钟」. diff --git a/design/components/core/core.card.html b/design/components/core/core.card.html new file mode 100644 index 0000000..36cb194 --- /dev/null +++ b/design/components/core/core.card.html @@ -0,0 +1,67 @@ + + + + + + + + + + + + + +
+
+ + + diff --git a/design/components/forms/Input.d.ts b/design/components/forms/Input.d.ts new file mode 100644 index 0000000..e98d531 --- /dev/null +++ b/design/components/forms/Input.d.ts @@ -0,0 +1,11 @@ +/** 文本输入框 — 桌面 36px / 移动 44px。 */ +export interface InputProps { + /** md=36px 桌面 / lg=44px 移动 @default 'md' */ + size?: 'md' | 'lg'; + placeholder?: string; + value?: string; + disabled?: boolean; + onChange?: (e: any) => void; + style?: any; +} +export declare function Input(props: InputProps): any; diff --git a/design/components/forms/Input.jsx b/design/components/forms/Input.jsx new file mode 100644 index 0000000..2eba74f --- /dev/null +++ b/design/components/forms/Input.jsx @@ -0,0 +1,26 @@ +const ddInputCss = ` +.dd-input { + display: block; width: 100%; box-sizing: border-box; + height: var(--control-md); padding: 0 12px; + font-family: var(--font-sans); font-size: var(--text-base); color: var(--text-1); + background: var(--surface-card); border: 1px solid var(--border-2); + border-radius: var(--radius-sm); + transition: border-color var(--dur-fast) var(--ease-out), box-shadow var(--dur-fast) var(--ease-out); +} +.dd-input::placeholder { color: var(--text-3); } +.dd-input:hover { border-color: var(--gray-400); } +.dd-input:focus { outline: none; border-color: var(--accent); box-shadow: var(--focus-ring); } +.dd-input[disabled] { opacity: 0.45; pointer-events: none; } +.dd-input--lg { height: var(--control-lg); font-size: var(--text-md); border-radius: var(--radius-sm); } +`; + +if (typeof document !== 'undefined' && !document.getElementById('dd-input-css')) { + const s = document.createElement('style'); + s.id = 'dd-input-css'; + s.textContent = ddInputCss; + document.head.appendChild(s); +} + +export function Input({ size = 'md', style, ...rest }) { + return ; +} diff --git a/design/components/forms/Input.prompt.md b/design/components/forms/Input.prompt.md new file mode 100644 index 0000000..40c125b --- /dev/null +++ b/design/components/forms/Input.prompt.md @@ -0,0 +1,7 @@ +Standard text field. Focus = accent border + soft ring. + +```jsx +const { Input } = window.DuduDesignSystem_2e0172; + + +``` diff --git a/design/components/forms/SettingRow.d.ts b/design/components/forms/SettingRow.d.ts new file mode 100644 index 0000000..7a5a712 --- /dev/null +++ b/design/components/forms/SettingRow.d.ts @@ -0,0 +1,9 @@ +/** 设置行 — 左标签(可带说明)+ 右控件,1px 分隔线;设置窗口/移动端「我的」的基本骨架。 */ +export interface SettingRowProps { + label: string; + /** 12px 灰色辅助说明 */ + description?: string; + /** 右侧控件:Switch / HotkeyCombo / Button / 文本 */ + children?: any; +} +export declare function SettingRow(props: SettingRowProps): any; diff --git a/design/components/forms/SettingRow.jsx b/design/components/forms/SettingRow.jsx new file mode 100644 index 0000000..eb23c40 --- /dev/null +++ b/design/components/forms/SettingRow.jsx @@ -0,0 +1,30 @@ +const ddSettingRowCss = ` +.dd-setrow { + display: flex; align-items: center; justify-content: space-between; gap: 16px; + padding: 12px 0; border-bottom: 1px solid var(--border-1); + font-family: var(--font-sans); font-size: var(--text-base); +} +.dd-setrow:last-child { border-bottom: none; } +.dd-setrow__label { color: var(--text-1); } +.dd-setrow__desc { font-size: var(--text-xs); color: var(--text-3); margin-top: 2px; } +.dd-setrow__control { display: flex; align-items: center; gap: 8px; color: var(--text-2); font-size: var(--text-sm); } +`; + +if (typeof document !== 'undefined' && !document.getElementById('dd-setrow-css')) { + const s = document.createElement('style'); + s.id = 'dd-setrow-css'; + s.textContent = ddSettingRowCss; + document.head.appendChild(s); +} + +export function SettingRow({ label, description, children, ...rest }) { + return ( +
+
+
{label}
+ {description ?
{description}
: null} +
+
{children}
+
+ ); +} diff --git a/design/components/forms/SettingRow.prompt.md b/design/components/forms/SettingRow.prompt.md new file mode 100644 index 0000000..33153ac --- /dev/null +++ b/design/components/forms/SettingRow.prompt.md @@ -0,0 +1,9 @@ +Label-left / control-right list row, the skeleton of every settings surface. + +```jsx +const { SettingRow, Switch, HotkeyCombo } = window.DuduDesignSystem_2e0172; + + + + +``` diff --git a/design/components/forms/Switch.d.ts b/design/components/forms/Switch.d.ts new file mode 100644 index 0000000..2a21798 --- /dev/null +++ b/design/components/forms/Switch.d.ts @@ -0,0 +1,7 @@ +/** 开关 — 设置项的布尔控件(开机自启、启用快捷键…)。 */ +export interface SwitchProps { + checked?: boolean; + onChange?: (next: boolean) => void; + disabled?: boolean; +} +export declare function Switch(props: SwitchProps): any; diff --git a/design/components/forms/Switch.jsx b/design/components/forms/Switch.jsx new file mode 100644 index 0000000..39cf9f8 --- /dev/null +++ b/design/components/forms/Switch.jsx @@ -0,0 +1,33 @@ +const ddSwitchCss = ` +.dd-switch { + position: relative; display: inline-block; width: 40px; height: 24px; + border-radius: var(--radius-full); background: var(--surface-3); + border: none; cursor: pointer; padding: 0; flex: none; + transition: background var(--dur-base) var(--ease-out); +} +.dd-switch::after { + content: ''; position: absolute; top: 3px; left: 3px; + width: 18px; height: 18px; border-radius: 50%; + background: #fff; box-shadow: 0 1px 3px rgba(17,19,26,0.25); + transition: transform var(--dur-base) var(--ease-out); +} +.dd-switch[aria-checked="true"] { background: var(--accent); } +.dd-switch[aria-checked="true"]::after { transform: translateX(16px); } +.dd-switch:focus-visible { outline: none; box-shadow: var(--focus-ring); } +.dd-switch[disabled] { opacity: 0.45; pointer-events: none; } +`; + +if (typeof document !== 'undefined' && !document.getElementById('dd-switch-css')) { + const s = document.createElement('style'); + s.id = 'dd-switch-css'; + s.textContent = ddSwitchCss; + document.head.appendChild(s); +} + +export function Switch({ checked = false, onChange, disabled = false, ...rest }) { + return ( + + ); +} diff --git a/design/components/forms/Switch.prompt.md b/design/components/forms/Switch.prompt.md new file mode 100644 index 0000000..412ba4a --- /dev/null +++ b/design/components/forms/Switch.prompt.md @@ -0,0 +1,6 @@ +Boolean toggle for settings rows. 40×24, accent when on, no bounce. + +```jsx +const { Switch } = window.DuduDesignSystem_2e0172; + +``` diff --git a/design/components/forms/forms.card.html b/design/components/forms/forms.card.html new file mode 100644 index 0000000..3d45afa --- /dev/null +++ b/design/components/forms/forms.card.html @@ -0,0 +1,42 @@ + + + + + + + + + + + + + +
+
+ + + diff --git a/design/components/voice/MicBar.d.ts b/design/components/voice/MicBar.d.ts new file mode 100644 index 0000000..e8f7a57 --- /dev/null +++ b/design/components/voice/MicBar.d.ts @@ -0,0 +1,13 @@ +/** + * 麦克风条 — 移动端键盘的主操作:按住说话的大胶囊按键。 + * @startingPoint section="Components" subtitle="按住说话 · 四种状态" viewport="700x300" + */ +export interface MicBarProps { + /** idle 按住说话 / recording 录音中 / disabled 未登录 / quota 超额 @default 'idle' */ + state?: 'idle' | 'recording' | 'disabled' | 'quota'; + /** 覆盖默认文案 */ + label?: string; + onPointerDown?: (e: any) => void; + onPointerUp?: (e: any) => void; +} +export declare function MicBar(props: MicBarProps): any; diff --git a/design/components/voice/MicBar.jsx b/design/components/voice/MicBar.jsx new file mode 100644 index 0000000..b1faaa0 --- /dev/null +++ b/design/components/voice/MicBar.jsx @@ -0,0 +1,54 @@ +import { Waveform } from './Waveform'; + +const ddMicBarCss = ` +.dd-micbar { + display: flex; align-items: center; justify-content: center; gap: 8px; + width: 100%; height: var(--control-xl); border: none; cursor: pointer; + border-radius: var(--radius-full); + font-family: var(--font-sans); font-size: var(--text-md); font-weight: var(--weight-semibold); + user-select: none; -webkit-user-select: none; touch-action: none; + transition: background var(--dur-fast) var(--ease-out), transform var(--dur-fast) var(--ease-out); +} +.dd-micbar--idle { background: var(--accent); color: var(--text-on-accent); } +.dd-micbar--idle:active { background: var(--accent-press); } +.dd-micbar--recording { background: var(--positive); color: #fff; } +.dd-micbar--disabled { background: var(--surface-3); color: var(--text-3); cursor: default; } +.dd-micbar--quota { background: var(--warning); color: #fff; } +`; + +if (typeof document !== 'undefined' && !document.getElementById('dd-micbar-css')) { + const s = document.createElement('style'); + s.id = 'dd-micbar-css'; + s.textContent = ddMicBarCss; + document.head.appendChild(s); +} + +function DdMicIcon({ size = 20 }) { + return ( + + ); +} + +const DD_MICBAR_LABELS = { + idle: '按住说话', + recording: '松开完成 · 上滑取消', + disabled: '打开 dudu App 登录', + quota: '今日试用已用完,去购买时长', +}; + +export function MicBar({ state = 'idle', label, ...rest }) { + const text = label || DD_MICBAR_LABELS[state] || DD_MICBAR_LABELS.idle; + return ( + + ); +} diff --git a/design/components/voice/MicBar.prompt.md b/design/components/voice/MicBar.prompt.md new file mode 100644 index 0000000..8875e57 --- /dev/null +++ b/design/components/voice/MicBar.prompt.md @@ -0,0 +1,11 @@ +The push-to-talk capsule that anchors the mobile keyboard. 56px tall, full width. + +```jsx +const { MicBar } = window.DuduDesignSystem_2e0172; + + // green + animated white waveform + // 未登录 + // 今日试用已用完 → 引导购买时长 +``` + +Default copy is built in; override with `label`. diff --git a/design/components/voice/RecognitionOverlay.d.ts b/design/components/voice/RecognitionOverlay.d.ts new file mode 100644 index 0000000..887b44f --- /dev/null +++ b/design/components/voice/RecognitionOverlay.d.ts @@ -0,0 +1,19 @@ +/** + * 识别浮层 — 桌面端核心界面:按住快捷键时出现在光标旁的深色小窗。 + * 两种主题下都保持深色。 + * @startingPoint section="Components" subtitle="桌面识别浮层 · 聆听/流式两态" viewport="700x220" + */ +export interface RecognitionOverlayProps { + /** listening 聆听中(未出字)/ streaming 流式识别中 @default 'listening' */ + state?: 'listening' | 'streaming'; + /** 已定稿文本(白色) */ + finalText?: string; + /** 未定稿 partial 文本(灰蓝色,实时刷新) */ + partialText?: string; + /** 右下角提示 @default '松开 ⌘⇧Space 完成输入' */ + hint?: string; + /** @default 340 */ + width?: number; + style?: any; +} +export declare function RecognitionOverlay(props: RecognitionOverlayProps): any; diff --git a/design/components/voice/RecognitionOverlay.jsx b/design/components/voice/RecognitionOverlay.jsx new file mode 100644 index 0000000..e4ea1dd --- /dev/null +++ b/design/components/voice/RecognitionOverlay.jsx @@ -0,0 +1,58 @@ +import { Waveform } from './Waveform'; + +const ddOverlayCss = ` +.dd-overlay { + box-sizing: border-box; width: 340px; + background: var(--overlay-bg); color: var(--overlay-text); + border-radius: var(--radius-lg); padding: 14px 18px; + box-shadow: var(--shadow-overlay); + font-family: var(--font-sans); + backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); +} +.dd-overlay__status { + display: flex; align-items: center; gap: 8px; + color: var(--overlay-text-2); font-size: var(--text-base); margin-top: 8px; +} +.dd-overlay__text { font-size: var(--text-md); line-height: 1.6; margin-top: 8px; min-height: 24px; } +.dd-overlay__text .dd-partial { color: var(--overlay-text-2); } +.dd-overlay__hint { font-size: 11px; color: var(--overlay-text-3); margin-top: 8px; text-align: right; } +`; + +if (typeof document !== 'undefined' && !document.getElementById('dd-overlay-css')) { + const s = document.createElement('style'); + s.id = 'dd-overlay-css'; + s.textContent = ddOverlayCss; + document.head.appendChild(s); +} + +export function RecognitionOverlay({ + state = 'listening', + finalText = '', + partialText = '', + hint = '松开 ⌘⇧Space 完成输入', + width = 340, + style, +}) { + return ( +
+ + {state === 'listening' ? ( +
+ + 聆听中… +
+ ) : ( +
+ {finalText} + {partialText ? {partialText} : null} +
+ )} +
{hint}
+
+ ); +} diff --git a/design/components/voice/RecognitionOverlay.prompt.md b/design/components/voice/RecognitionOverlay.prompt.md new file mode 100644 index 0000000..e266585 --- /dev/null +++ b/design/components/voice/RecognitionOverlay.prompt.md @@ -0,0 +1,11 @@ +The desktop push-to-talk overlay: dark glass panel (in BOTH themes), waveform on top, recognized text below, hotkey hint bottom-right. + +```jsx +const { RecognitionOverlay } = window.DuduDesignSystem_2e0172; + + +``` + +finalText = settled (white); partialText = in-flight (gray-blue). Never restyle it light. diff --git a/design/components/voice/Waveform.d.ts b/design/components/voice/Waveform.d.ts new file mode 100644 index 0000000..81f9d68 --- /dev/null +++ b/design/components/voice/Waveform.d.ts @@ -0,0 +1,13 @@ +/** 声波 — 品牌母题:竖向圆头短线组成的音量波形;录音中起伏动画。 */ +export interface WaveformProps { + /** 竖线条数 @default 12 */ + bars?: number; + /** 录音中(起伏动画) @default false */ + active?: boolean; + /** 最大波高 px @default 26 */ + height?: number; + /** 波形颜色 @default 'var(--accent)' */ + color?: string; + style?: any; +} +export declare function Waveform(props: WaveformProps): any; diff --git a/design/components/voice/Waveform.jsx b/design/components/voice/Waveform.jsx new file mode 100644 index 0000000..6981271 --- /dev/null +++ b/design/components/voice/Waveform.jsx @@ -0,0 +1,35 @@ +const ddWaveCss = ` +.dd-wave { display: inline-flex; align-items: center; gap: 3px; } +.dd-wave i { display: block; width: 3px; border-radius: 2px; background: currentColor; } +.dd-wave--active i { animation: dd-wave-bounce 0.9s var(--ease-in-out) infinite alternate; } +@keyframes dd-wave-bounce { + from { transform: scaleY(0.35); } + to { transform: scaleY(1); } +} +@media (prefers-reduced-motion: reduce) { + .dd-wave--active i { animation: none; } +} +`; + +if (typeof document !== 'undefined' && !document.getElementById('dd-wave-css')) { + const s = document.createElement('style'); + s.id = 'dd-wave-css'; + s.textContent = ddWaveCss; + document.head.appendChild(s); +} + +const DD_WAVE_PATTERN = [0.3, 0.55, 0.85, 0.45, 0.7, 1, 0.4, 0.6, 0.25, 0.8, 0.5, 0.35]; + +export function Waveform({ bars = 12, active = false, height = 26, color = 'var(--accent)', style }) { + const items = []; + for (let i = 0; i < bars; i++) { + const h = Math.round(DD_WAVE_PATTERN[i % DD_WAVE_PATTERN.length] * height); + items.push(); + } + return ( + + ); +} diff --git a/design/components/voice/Waveform.prompt.md b/design/components/voice/Waveform.prompt.md new file mode 100644 index 0000000..c1d0489 --- /dev/null +++ b/design/components/voice/Waveform.prompt.md @@ -0,0 +1,8 @@ +The brand's voice motif — vertical rounded bars. Animates only while `active` (recording). + +```jsx +const { Waveform } = window.DuduDesignSystem_2e0172; + // 录音中,品牌蓝 + // 深色浮层内用亮蓝 + // 静态装饰 +``` diff --git a/design/components/voice/voice.card.html b/design/components/voice/voice.card.html new file mode 100644 index 0000000..15a7cac --- /dev/null +++ b/design/components/voice/voice.card.html @@ -0,0 +1,42 @@ + + + + + + + + + + + + + +
+
+ + + diff --git a/design/guidelines/brand-logo.card.html b/design/guidelines/brand-logo.card.html new file mode 100644 index 0000000..b56e211 --- /dev/null +++ b/design/guidelines/brand-logo.card.html @@ -0,0 +1,23 @@ + + + + + + + + + +
dudu logo
+
dudu logo blue
+
dudu logo white
+
dudu app icon
+ + diff --git a/design/guidelines/brand-wordmark.card.html b/design/guidelines/brand-wordmark.card.html new file mode 100644 index 0000000..0e2d20c --- /dev/null +++ b/design/guidelines/brand-wordmark.card.html @@ -0,0 +1,24 @@ + + + + + + + + + +
+ + +
+
+ + +
+ + diff --git a/design/guidelines/colors-brand.card.html b/design/guidelines/colors-brand.card.html new file mode 100644 index 0000000..192656e --- /dev/null +++ b/design/guidelines/colors-brand.card.html @@ -0,0 +1,31 @@ + + + + + + + + + +
+
50
+
100
+
200
+
300
+
400
+
500 ★
+
600
+
700
+
800
+
900
+
+ + diff --git a/design/guidelines/colors-neutrals.card.html b/design/guidelines/colors-neutrals.card.html new file mode 100644 index 0000000..345edff --- /dev/null +++ b/design/guidelines/colors-neutrals.card.html @@ -0,0 +1,33 @@ + + + + + + + + + +
+
0
+
25
+
50
+
100
+
200
+
300
+
400
+
500
+
600
+
700
+
800
+
900
+
950
+
+ + diff --git a/design/guidelines/colors-overlay.card.html b/design/guidelines/colors-overlay.card.html new file mode 100644 index 0000000..925669d --- /dev/null +++ b/design/guidelines/colors-overlay.card.html @@ -0,0 +1,30 @@ + + + + + + + + + +
+
+
帮我把这份周报整理一下,重点突出本周的
+
松开 ⌘⇧Space 完成输入
+
+
+
+
+
帮我把这份周报整理一下,重点突出本周的
+
松开 ⌘⇧Space 完成输入
+
+
+ + diff --git a/design/guidelines/colors-semantic.card.html b/design/guidelines/colors-semantic.card.html new file mode 100644 index 0000000..2a29f87 --- /dev/null +++ b/design/guidelines/colors-semantic.card.html @@ -0,0 +1,28 @@ + + + + + + + + + +
+
录音中 · 余额充足--positive
+
时长将用完--warning
+
连接失败--danger
+
+
+
录音中 · 余额充足--positive
+
时长将用完--warning
+
连接失败--danger
+
+ + diff --git a/design/guidelines/colors-surfaces.card.html b/design/guidelines/colors-surfaces.card.html new file mode 100644 index 0000000..7839162 --- /dev/null +++ b/design/guidelines/colors-surfaces.card.html @@ -0,0 +1,44 @@ + + + + + + + + + +
+
+
text-1 · 主要文字
+
text-2 · 次要说明
+
text-3 · 弱提示
+
+
bg-app
+
surface-2
+
surface-3
+
+
+
+
+
+
text-1 · 主要文字
+
text-2 · 次要说明
+
text-3 · 弱提示
+
+
bg-app
+
surface-2
+
surface-3
+
+
+
+ + diff --git a/design/guidelines/motion.card.html b/design/guidelines/motion.card.html new file mode 100644 index 0000000..24d46b7 --- /dev/null +++ b/design/guidelines/motion.card.html @@ -0,0 +1,24 @@ + + + + + + + + + +
--dur-fast · 120ms
hover · 按压
+
--dur-base · 200ms
浮层 · 开关
+
--dur-slow · 320ms
屏幕切换
+ + diff --git a/design/guidelines/spacing-controls.card.html b/design/guidelines/spacing-controls.card.html new file mode 100644 index 0000000..d91e320 --- /dev/null +++ b/design/guidelines/spacing-controls.card.html @@ -0,0 +1,20 @@ + + + + + + + + + +
紧凑 28
--control-sm
+
桌面默认 36
--control-md
+
移动最小触控 44
--control-lg
+
主操作 / 麦克风条 56
--control-xl
+ + diff --git a/design/guidelines/spacing-elevation.card.html b/design/guidelines/spacing-elevation.card.html new file mode 100644 index 0000000..4f05d15 --- /dev/null +++ b/design/guidelines/spacing-elevation.card.html @@ -0,0 +1,30 @@ + + + + + + + + + +
+
card
+
menu
+
window
+
overlay
+
+
+
card
+
menu
+
window
+
overlay
+
+ + diff --git a/design/guidelines/spacing-grid.card.html b/design/guidelines/spacing-grid.card.html new file mode 100644 index 0000000..b867f3d --- /dev/null +++ b/design/guidelines/spacing-grid.card.html @@ -0,0 +1,26 @@ + + + + + + + + + +
1·4
+
2·8
+
3·12
+
4·16
+
5·20
+
6·24
+
8·32
+
10·40
+
12·48
+
16·64
+ + diff --git a/design/guidelines/spacing-radius.card.html b/design/guidelines/spacing-radius.card.html new file mode 100644 index 0000000..45cc23a --- /dev/null +++ b/design/guidelines/spacing-radius.card.html @@ -0,0 +1,23 @@ + + + + + + + + + +
xs · 4 · kbd
+
sm · 6 · 按钮
+
md · 10 · 菜单
+
lg · 14 · 卡片/浮层
+
xl · 20 · 大容器
+
full · 麦克风条
+ + diff --git a/design/guidelines/type-families.card.html b/design/guidelines/type-families.card.html new file mode 100644 index 0000000..ad66ecc --- /dev/null +++ b/design/guidelines/type-families.card.html @@ -0,0 +1,31 @@ + + + + + + + + + +
+ --font-sans + dudu 0123456789 + Outfit · 字标与数字 +
+
+ --font-sans + 嘟嘟语音输入,说话即上屏 + 中文走系统栈 PingFang SC / MiSans +
+
+ --font-mono + /v1/asr/stream · 00:04 + 代码 · 计时 +
+ + diff --git a/design/guidelines/type-recognition.card.html b/design/guidelines/type-recognition.card.html new file mode 100644 index 0000000..57a005f --- /dev/null +++ b/design/guidelines/type-recognition.card.html @@ -0,0 +1,32 @@ + + + + + + + + + +
+
好的,我马上把文件发给你,大概十分钟之
+
键盘 partial 文本条:final 用 text-1,partial 用 text-3
+
+
+
好的,我马上把文件发给你,大概十分钟之
+
浮层:final 用 overlay-text,partial 用 overlay-text-2
+
+ + diff --git a/design/guidelines/type-scale.card.html b/design/guidelines/type-scale.card.html new file mode 100644 index 0000000..1084239 --- /dev/null +++ b/design/guidelines/type-scale.card.html @@ -0,0 +1,24 @@ + + + + + + + + + +
--text-xs 12角标 · 键帽 · 浮层提示
+
--text-sm 13次要信息 · 托盘菜单
+
--text-base 14桌面正文 · 设置项
+
--text-md 15浮层识别文本 · 移动正文
+
--text-lg 17区块标题 · 移动列表
+
--text-xl 20窗口标题
+
--text-2xl 24页面大标题
+
--text-3xl 32¥ 128 / 年
+ + diff --git a/design/readme.md b/design/readme.md new file mode 100644 index 0000000..92de0cb --- /dev/null +++ b/design/readme.md @@ -0,0 +1,101 @@ +# dudu 语音输入法 · 设计系统 + +**dudu(嘟嘟)** 是一款付费语音输入法,核心卖点:大模型识别准确度 + 流式实时上屏 + 极简交互。全平台覆盖(Mac / Windows / iPhone / iPad / Android),桌面端按住全局快捷键说话、移动端键盘按住大麦克风说话,微信登录 + 微信支付按识别时长购买(时长包,不过期),每天免费试用 3 分钟。 + +**设计基调:简约、克制、快。** 输入法是"消失在使用中"的工具——界面越少越好,反馈越快越好。支持 Light / Dark 双模式,桌面与移动端共用同一套令牌。 + +## 来源 + +- 本地代码库 `dudu/`(File System Access 挂载)— 目前仅含 `dudu/doc/plan/design.html`:MVP 前后端设计方案 v0.1(2026-06-11),内含全部界面清单与粗略 mockup。本设计系统的页面设计以该文档第八章为需求来源。 +- 技术栈(影响设计决策):桌面 Tauri 2 + Rust(浮层不抢焦点)、移动端原生键盘扩展(iOS/Android)、后端 Go。 + +## 产品表面 + +| 表面 | 界面 | +|---|---| +| 桌面端(Mac/Win) | ① 识别浮层(核心,两态)② 系统托盘菜单 ③ 设置窗口 ④ 登录/购买窗口 | +| 移动端键盘扩展 | ① 语音为主键盘(大麦克风)② partial 文本条 ③ 未登录/超额提示态 | +| 移动端主 App | ① 启用引导 ② 微信登录 ③ 购买时长 ④ 我的(余额/试用/权限) | + +## 品牌标识 + +"嘟嘟"= 撅起嘴发出的声音。Logo 以**圆形嘴巴**为母题:一个线条圆环(嘟起的嘴)+ 内部三条声波竖线(说话中)。纯线条、3.5px 圆头笔画、单色 `currentColor`。字标 "dudu" 用同一笔画语言手绘(圆 + 竖线 = d/u)。 + +- `assets/logo-mark.svg` — 标准线条 mark(currentColor,随前景色) +- `assets/logo-mark-ink.svg` / `-blue.svg` / `-white.svg` — 固定色变体 +- `assets/wordmark.svg` — "dudu" 字标(currentColor) +- `assets/app-icon.svg` / `app-icon-dark.svg` — 圆角方形应用图标(22/96 圆角比) +- 中文标准写法:"dudu 语音输入"或"嘟嘟语音输入";品牌名永远小写 **dudu** + +## CONTENT FUNDAMENTALS(文案基调) + +- **语言**:简体中文为主;品牌名小写 dudu。技术词(partial/final)不出现在用户界面,界面上只说"实时识别""上屏"。 +- **人称**:对用户用"你",不用"您"(简约、平等、工具感)。 +- **句式**:短句、动宾结构、无营销腔。"按住说话""松开完成""上滑取消"——三到六个字说清一个动作。 +- **标点**:界面微文案不带句号;提示语用"·"分隔("松开完成 · 上滑取消")。 +- **数字**:半角数字 + 空格:"472 / 600 分钟""00:04"。 +- **emoji**:界面文案不用 emoji(规划稿 mockup 里的 🎙️ 在正式设计中为线条 mic 图标)。 +- **示例语料**(演示识别文本用):日常办公对话——"帮我把这份周报整理一下,重点突出本周的进展""好的,我马上把文件发给你"。 + +## VISUAL FOUNDATIONS(视觉基础) + +- **色彩**:单一品牌蓝 `--accent`(light #4F6EF7 / dark #6E89F9)+ 冷灰中性阶梯。绿=录音中/成功,琥珀=超额/警告,红=错误。大面积界面是中性色,蓝色只出现在主操作与品牌处——**一屏最多一个蓝色主按钮**。**禁用渐变**(规划稿的蓝紫渐变弃用),纯色填充。 +- **双模式**:light 页面底 `#F6F7FA` + 白卡片;dark 页面底 `#0F1116` + `#171A22` 卡片。识别浮层在两种模式下都是深色玻璃(`--overlay-bg` 95% 不透明 + blur(12px))——它叠在任意用户界面上,深色最不打扰。 +- **字体**:中文 UI 走系统字体(PingFang SC / MiSans / HarmonyOS Sans / 雅黑);Outfit 仅用于 "dudu" 字标、数字与价格。正文 14px(桌面)/ 15px(移动与浮层),微文案 12–13px。字重 400/500/600(700 仅大数字)。 +- **间距**:4px 网格;卡片内边距 16–24px;控件高度 28/36px(桌面)、44/56px(移动触控)。 +- **圆角**:呼应圆嘴 logo——按钮/输入框 6px、菜单 10px、卡片/浮层 14px、大容器 20px、麦克风条/胶囊 999px。 +- **边框与阴影**:1px 实线边框(`--border-1`)是主要分隔手段;页面内卡片仅 `--shadow-card`(极轻);浮起元素用 `--shadow-menu/window/overlay` 三档。dark 模式阴影加深、靠边框提层级。无内阴影。 +- **背景**:纯色,无图片、无纹理、无插画。空状态用 logo 线条图形 + 一句话。 +- **动效**:快入慢出 `--ease-out`,120/200/320ms 三档;浮层淡入+上移 4px;波形条高度动画是唯一循环动画(仅录音中)。**无弹跳、无旋转、无视差**。 +- **Hover**:背景加一层(surface-2/3);主按钮加深为 `--accent-hover`。**按压**:再深一档 `--accent-press`,桌面无缩放;移动端键帽按压可缩 0.97。 +- **焦点**:3px 柔和蓝环 `--focus-ring`,仅键盘聚焦显示。 +- **透明与模糊**:仅两处——识别浮层与托盘菜单(95–98% 不透明 + blur)。普通卡片不透明。 +- **图片色调**:产品几乎无摄影图;若营销需要,冷色调、低饱和、留白多。 +- **卡片范式**:`--surface-card` + 1px `--border-1` + 14px 圆角 + `--shadow-card`;浮起卡片才用更重阴影。 + +## ICONOGRAPHY(图标) + +- **图标系统**:[Lucide](https://lucide.dev)(ISC 许可)线条图标——24 网格、2px 圆头描边,与线条 logo 同语言。组件内已内嵌所需 Lucide 路径(mic、settings 等),新界面可从 lucide.dev 复制同风格路径或经 CDN `unpkg.com/lucide-static` 引用。 +- 描边统一 2px,颜色跟随 `currentColor`。 +- **不用 emoji 作图标**;不用 unicode 符号作图标。例外:键帽符号 ⌘ ⇧ ⌫ ↵ ——它们是键盘标准字符,出现在 `Kbd` / `HotkeyCombo` 组件中。 +- 无 png 图标,全部 SVG。 + +## 索引 + +``` +styles.css 全局入口(@import 全部令牌) +tokens/ colors / typography / spacing / effects / fonts +assets/ logo-mark(.ink/.blue/.white) · wordmark · app-icon(±dark) +guidelines/ 基础规范卡片(品牌、色板、字体、间距、动效) +components/core/ Button · IconButton · Badge · Kbd · HotkeyCombo · Card · ProgressBar +components/forms/ Input · Switch · SettingRow +components/voice/ Waveform · MicBar · RecognitionOverlay +ui_kits/desktop/ 桌面端:识别浮层 / 托盘菜单 / 设置 / 登录购买(交互演示) +ui_kits/mobile/ 移动端:语音键盘(多态)+ 主 App(引导 / 购买 / 我的) +SKILL.md 供 Claude Code 等代理使用的技能入口 +``` + +### 组件速查 + +| 组件 | 关键 props | +|---|---| +| Button | `variant: primary/secondary/ghost/danger` · `size: sm/md/lg` · `block` | +| IconButton | `size` · `label`(无障碍必填)| +| Badge | `tone: green/blue/orange/red/neutral` | +| Kbd / HotkeyCombo | `keys={['⌘','⇧','Space']}` | +| Card | `padding` | +| ProgressBar | `value/max` · `warnAt=0.9`(超阈值自动变警告色)| +| Input | `size: md/lg` | +| Switch | `checked` · `onChange` | +| SettingRow | `label` · `description` · children 放右侧控件 | +| Waveform | `bars` · `active` · `height` · `color` | +| MicBar | `state: idle/recording/disabled/quota`(文案内置)| +| RecognitionOverlay | `state: listening/text` · `finalText` · `partialText` · `hint` | + +### 使用规则速查 + +1. 先链 `styles.css`;dark 模式给容器加 `data-theme="dark"`。 +2. 一屏一个蓝色主按钮;其余操作 secondary/ghost。 +3. 浮层永远深色玻璃;键盘/设置跟随系统模式。 +4. 文案:短、用"你"、无句号、无 emoji。 +5. 不画渐变,不加无意义阴影,不引入第二品牌色。 diff --git a/design/styles.css b/design/styles.css new file mode 100644 index 0000000..367e279 --- /dev/null +++ b/design/styles.css @@ -0,0 +1,5 @@ +@import "./tokens/fonts.css"; +@import "./tokens/colors.css"; +@import "./tokens/typography.css"; +@import "./tokens/spacing.css"; +@import "./tokens/effects.css"; diff --git a/design/tokens/colors.css b/design/tokens/colors.css new file mode 100644 index 0000000..f38cad2 --- /dev/null +++ b/design/tokens/colors.css @@ -0,0 +1,113 @@ +/* ============================================================ + dudu 语音输入法 · 颜色令牌 + Light 为默认(:root),Dark 通过 [data-theme="dark"] 切换。 + 简约原则:单一品牌蓝 + 冷灰中性阶梯;绿=录音/成功, + 琥珀=超额/警告,红=错误。识别浮层两种模式均为深色玻璃。 + ============================================================ */ + +:root { + /* ---- 品牌蓝阶梯 ---- */ + --blue-50: #EEF1FE; + --blue-100: #DFE5FD; + --blue-200: #C4CFFB; + --blue-300: #9FB1F9; + --blue-400: #7590F8; + --blue-500: #4F6EF7; + --blue-600: #3D58DB; + --blue-700: #2F44B3; + --blue-800: #25368C; + --blue-900: #1E2B6B; + + /* ---- 中性阶梯(冷灰)---- */ + --gray-0: #FFFFFF; + --gray-25: #FCFCFD; + --gray-50: #F6F7FA; + --gray-100: #F0F1F4; + --gray-200: #E4E6EB; + --gray-300: #D2D5DD; + --gray-400: #A6ABB8; + --gray-500: #7A8090; + --gray-600: #5A6072; + --gray-700: #434957; + --gray-800: #2B2F3A; + --gray-900: #1B1E26; + --gray-950: #11131A; + + /* ---- 功能色 ---- */ + --green-500: #16A34A; + --green-100: #DCFCE7; + --amber-500: #E8890C; + --amber-100: #FCEFD7; + --red-500: #DC2626; + --red-100: #FEE2E2; + + /* ============ 语义别名 · Light ============ */ + --bg-app: var(--gray-50); + --surface-card: var(--gray-0); + --surface-2: var(--gray-100); + --surface-3: var(--gray-200); + --border-1: var(--gray-200); + --border-2: var(--gray-300); + + --text-1: var(--gray-900); + --text-2: var(--gray-600); + --text-3: var(--gray-400); + --text-on-accent: #FFFFFF; + + --accent: var(--blue-500); + --accent-hover: var(--blue-600); + --accent-press: var(--blue-700); + --accent-soft: var(--blue-50); + --accent-soft-2: var(--blue-100); + --accent-text: var(--blue-600); + + --positive: var(--green-500); + --positive-soft: var(--green-100); + --warning: var(--amber-500); + --warning-soft: var(--amber-100); + --danger: var(--red-500); + --danger-soft: var(--red-100); + + /* 识别浮层(两种模式均为深色玻璃)*/ + --overlay-bg: rgba(24, 26, 34, 0.95); + --overlay-text: #F2F4FA; + --overlay-text-2: #9AA3BD; + --overlay-text-3: #646E8C; + + --focus-ring: 0 0 0 3px var(--blue-200); +} + +[data-theme="dark"] { + --bg-app: #0F1116; + --surface-card: #171A22; + --surface-2: #1E222C; + --surface-3: #262B37; + --border-1: #272C38; + --border-2: #343A49; + + --text-1: #ECEEF4; + --text-2: #9AA1B2; + --text-3: #5F6678; + --text-on-accent: #FFFFFF; + + --accent: #5C77E8; + --accent-hover: #6E89F9; + --accent-press: #4A63D6; + --accent-soft: rgba(92, 119, 232, 0.16); + --accent-soft-2: rgba(92, 119, 232, 0.26); + --accent-text: #7D95F7; + + --positive: #34C46A; + --positive-soft: rgba(52, 196, 106, 0.16); + --warning: #F2A33C; + --warning-soft: rgba(242, 163, 60, 0.16); + --danger: #F05B5B; + --danger-soft: rgba(240, 91, 91, 0.16); + + --overlay-bg: rgba(30, 33, 43, 0.96); + --overlay-text: #F2F4FA; + --overlay-text-2: #9AA3BD; + --overlay-text-3: #646E8C; + + --focus-ring: 0 0 0 3px rgba(92, 119, 232, 0.4); +} diff --git a/design/tokens/effects.css b/design/tokens/effects.css new file mode 100644 index 0000000..a149647 --- /dev/null +++ b/design/tokens/effects.css @@ -0,0 +1,29 @@ +/* ============================================================ + dudu · elevation / motion tokens + 阴影极轻——简约风靠边框和层级背景色区分层次; + 只有浮动元素(浮层、菜单、弹窗)有明显投影。 + ============================================================ */ + +:root { + /* elevation */ + --shadow-card: 0 1px 3px rgba(17, 19, 26, 0.05); + --shadow-menu: 0 4px 16px rgba(17, 19, 26, 0.10), 0 1px 3px rgba(17, 19, 26, 0.06); + --shadow-window: 0 8px 28px rgba(17, 19, 26, 0.14); + --shadow-overlay: 0 12px 36px rgba(10, 12, 18, 0.38); + --shadow-key: 0 1px 0 rgba(17, 19, 26, 0.22); /* 键帽底边 */ + + /* motion — 快入慢出,无弹跳 */ + --ease-out: cubic-bezier(0.2, 0.8, 0.2, 1); /* @kind other */ + --ease-in-out: cubic-bezier(0.45, 0, 0.2, 1); /* @kind other */ + --dur-fast: 120ms; /* @kind other */ /* hover、按压 */ + --dur-base: 200ms; /* @kind other */ /* 浮层进出、开关 */ + --dur-slow: 320ms; /* @kind other */ /* 页面/屏幕切换 */ +} + +[data-theme="dark"] { + --shadow-card: 0 1px 3px rgba(0, 0, 0, 0.3); + --shadow-menu: 0 4px 16px rgba(0, 0, 0, 0.4), 0 1px 3px rgba(0, 0, 0, 0.3); + --shadow-window: 0 8px 28px rgba(0, 0, 0, 0.5); + --shadow-overlay: 0 12px 36px rgba(0, 0, 0, 0.6); + --shadow-key: 0 1px 0 rgba(0, 0, 0, 0.5); +} diff --git a/design/tokens/fonts.css b/design/tokens/fonts.css new file mode 100644 index 0000000..9560830 --- /dev/null +++ b/design/tokens/fonts.css @@ -0,0 +1,6 @@ +/* dudu · 品牌字体 + Outfit(Google Fonts CDN)— 圆润几何无衬线,用于品牌拉丁字 + "dudu"、数字与价格;中文 UI 文本走系统字体栈(PingFang SC / + MiSans / HarmonyOS Sans / 雅黑)。 + 注:暂以 CDN 引入;如有自托管字体文件请替换为 @font-face。 */ +@import url("https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&display=swap"); diff --git a/design/tokens/spacing.css b/design/tokens/spacing.css new file mode 100644 index 0000000..dc1b846 --- /dev/null +++ b/design/tokens/spacing.css @@ -0,0 +1,33 @@ +/* ============================================================ + dudu · 间距 / 圆角 / 控件尺寸令牌 + 4px 基准网格;圆角偏圆润(呼应圆嘴 logo); + 控件高度:桌面紧凑 28/36,移动触控 44/56。 + ============================================================ */ + +:root { + /* 间距(4px 网格)*/ + --space-1: 4px; + --space-2: 8px; + --space-3: 12px; + --space-4: 16px; + --space-5: 20px; + --space-6: 24px; + --space-8: 32px; + --space-10: 40px; + --space-12: 48px; + --space-16: 64px; + + /* 圆角 */ + --radius-xs: 4px; /* kbd、小标签 */ + --radius-sm: 6px; /* 按钮、输入框、键帽 */ + --radius-md: 10px; /* 菜单、小卡片 */ + --radius-lg: 14px; /* 卡片、浮层 */ + --radius-xl: 20px; /* 大容器、手机屏 */ + --radius-full: 999px; + + /* 控件高度 */ + --control-sm: 28px; /* 桌面紧凑 */ + --control-md: 36px; /* 桌面默认 */ + --control-lg: 44px; /* 移动最小触控 */ + --control-xl: 56px; /* 移动主操作 / 麦克风条 */ +} diff --git a/design/tokens/typography.css b/design/tokens/typography.css new file mode 100644 index 0000000..a52c802 --- /dev/null +++ b/design/tokens/typography.css @@ -0,0 +1,38 @@ +/* ============================================================ + dudu · 字体与排版令牌 + UI 中文走系统字体(输入法 = 原生应用,跟随系统最自然); + Outfit(圆润几何无衬线)用于拉丁字 "dudu"、数字与价格, + 与圆嘴 logo 的线条语言呼应。字重只用 400/500/600/700。 + ============================================================ */ + +:root { + --font-sans: 'Outfit', -apple-system, BlinkMacSystemFont, 'PingFang SC', + 'MiSans', 'HarmonyOS Sans SC', 'Microsoft YaHei', 'Segoe UI', sans-serif; + --font-mono: ui-monospace, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace; + + /* 字号阶梯 */ + --text-xs: 12px; /* 角标、键帽 */ + --text-sm: 13px; /* 次要信息、菜单 */ + --text-base: 14px; /* 桌面正文 */ + --text-md: 15px; /* 浮层识别文本、移动正文 */ + --text-lg: 17px; /* 区块标题、移动列表 */ + --text-xl: 20px; /* 窗口标题 */ + --text-2xl: 24px; /* 页面大标题 */ + --text-3xl: 32px; /* 价格、大数字 */ + --text-4xl: 44px; /* 营销大字 */ + + /* 字重 */ + --weight-regular: 400; + --weight-medium: 500; + --weight-semibold: 600; + --weight-bold: 700; + + /* 行高 */ + --leading-tight: 1.25; + --leading-normal: 1.5; + --leading-loose: 1.75; /* 中文长文本 */ + + /* 字距 */ + --tracking-normal: 0; + --tracking-wide: 0.02em; /* 品牌字 dudu */ +} diff --git a/design/ui_kits/desktop/DesktopShared.jsx b/design/ui_kits/desktop/DesktopShared.jsx new file mode 100644 index 0000000..d9eb652 --- /dev/null +++ b/design/ui_kits/desktop/DesktopShared.jsx @@ -0,0 +1,124 @@ +// dudu 桌面端 UI kit · 共享元素:Lucide 图标子集、mac 窗口框、伪二维码 +// 通过 window 共享给其余 text/babel 脚本 + +function DuduIcon({ d, size = 16, sw = 2, children, ...rest }) { + return ( + + ); +} + +const DuIcons = { + Mic: (p) => ( + + + + + + ), + Settings: (p) => ( + + + + + ), + Check: (p) => , + X: (p) => , + ChevronDown: (p) => , + ChevronRight: (p) => , + Sun: (p) => ( + + + + + + + ), + Moon: (p) => , + AudioLines: (p) => ( + + + + + ), + User: (p) => ( + + + + + ), + LogOut: (p) => ( + + + + + + ), +}; + +// dudu 圆嘴 logo(currentColor 线条版) +function DuduLogo({ size = 20, sw = 3.5 }) { + return ( + + ); +} + +// mac 风格窗口(红绿灯 + 标题) +function MacWindow({ title, width = 460, children, style }) { + return ( +
+
+ + + + {title} +
+ {children} +
+ ); +} + +// 伪二维码占位(确定性图案,仅作占位 — 接入后端 /v1/auth/qr 后替换) +function FakeQr({ size = 148, seed = 7 }) { + const n = 17; + const cells = []; + let s = seed; + const rnd = () => { s = (s * 16807) % 2147483647; return s / 2147483647; }; + for (let y = 0; y < n; y++) { + for (let x = 0; x < n; x++) { + const corner = (x < 5 && y < 5) || (x > n - 6 && y < 5) || (x < 5 && y > n - 6); + let on; + if (corner) { + const cx = x > n - 6 ? x - (n - 5) : x, cy = y > n - 6 ? y - (n - 5) : y; + on = cx === 0 || cx === 4 || cy === 0 || cy === 4 || (cx > 1 && cx < 3 + 1 && cy > 1 && cy < 3 + 1); + } else { + on = rnd() > 0.52; + } + if (on) cells.push(); + } + } + return ( +
{cells}
+ ); +} + +Object.assign(window, { DuduIcon, DuIcons, DuduLogo, MacWindow, FakeQr }); diff --git a/design/ui_kits/desktop/LoginPurchase.jsx b/design/ui_kits/desktop/LoginPurchase.jsx new file mode 100644 index 0000000..b9ecfe0 --- /dev/null +++ b/design/ui_kits/desktop/LoginPurchase.jsx @@ -0,0 +1,108 @@ +// dudu 桌面端 · 登录 / 购买窗口(点击走完整流程:扫码登录 → 选时长包 → 支付 → 完成) + +function LoginPurchaseWindow() { + const { Button, Badge } = window.DuduDesignSystem_2e0172; + const [step, setStep] = React.useState('login'); // login | plans | pay | done + const [plan, setPlan] = React.useState('m'); + + const PACKS = { + s: { label: '100 分钟', price: 9, desc: '约 ¥0.09 / 分钟', tag: '' }, + m: { label: '500 分钟', price: 39, desc: '约 ¥0.078 / 分钟', tag: '省 13%' }, + l: { label: '2000 分钟', price: 129, desc: '约 ¥0.065 / 分钟', tag: '省 28%' }, + }; + + const Center = ({ children }) => ( +
+ {children} +
+ ); + + return ( + + {step === 'login' && ( +
+ +
微信扫码登录
+ +
+ 打开微信扫一扫,确认后自动登录 +
+ +
+ )} + + {step === 'plans' && ( +
+
+ 购买时长 + + wang*** 试用中 + +
+ +
+ {Object.entries(PACKS).map(([key, p]) => { + const active = plan === key; + return ( + + ); + })} +
+ +
+ 时长不过期 · 每天另有 3 分钟免费试用 +
+ +
+ )} + + {step === 'pay' && ( +
+
微信扫码支付
+
+ ¥{PACKS[plan].price} · {PACKS[plan].label}时长包 +
+ +
支付完成后自动开通
+
+ + +
+
+ )} + + {step === 'done' && ( +
+ +
已到账 {PACKS[plan].label}
+
+ 时长余额 472 分钟 · 不过期 · 现在就按住 ⌘⇧Space 试试 +
+ +
+ )} +
+ ); +} + +Object.assign(window, { LoginPurchaseWindow }); diff --git a/design/ui_kits/desktop/OverlayDemo.jsx b/design/ui_kits/desktop/OverlayDemo.jsx new file mode 100644 index 0000000..49bf26c --- /dev/null +++ b/design/ui_kits/desktop/OverlayDemo.jsx @@ -0,0 +1,108 @@ +// dudu 桌面端 · 识别浮层演示 +// 在仿真备忘录窗口上方演示完整 push-to-talk 流程: +// 按住按钮(= 按住 ⌘⇧Space)→ 聆听中 → partial 流式 → 松开 → final 注入输入框 + +const DEMO_SENTENCES = [ + '帮我把这份周报整理一下,重点突出本周的进展和下周的计划。', + '好的,我马上把文件发给你,大概十分钟之内。', + '明天上午十点的会改到下午三点,记得同步给项目组。', +]; + +function OverlayDemo() { + const { RecognitionOverlay } = window.DuduDesignSystem_2e0172; + const [phase, setPhase] = React.useState('idle'); // idle | listening | streaming + const [finalText, setFinalText] = React.useState(''); + const [partialText, setPartialText] = React.useState(''); + const [docText, setDocText] = React.useState(''); + const [sentenceIdx, setSentenceIdx] = React.useState(0); + const timerRef = React.useRef(null); + const stateRef = React.useRef({}); + stateRef.current = { finalText, partialText, sentenceIdx }; + + const stop = React.useCallback(() => { + clearInterval(timerRef.current); + const { finalText: f, partialText: p, sentenceIdx: idx } = stateRef.current; + const committed = f + p; + if (committed) { + setDocText((d) => d + committed); + setSentenceIdx((idx + 1) % DEMO_SENTENCES.length); + } + setFinalText(''); setPartialText(''); + setPhase('idle'); + }, []); + + const start = React.useCallback(() => { + if (timerRef.current) clearInterval(timerRef.current); + setPhase('listening'); setFinalText(''); setPartialText(''); + const sentence = DEMO_SENTENCES[stateRef.current.sentenceIdx]; + let i = 0; + setTimeout(() => setPhase((ph) => (ph === 'listening' ? 'streaming' : ph)), 420); + timerRef.current = setInterval(() => { + i += 1; + if (i >= sentence.length) { clearInterval(timerRef.current); setFinalText(sentence); setPartialText(''); return; } + // 模拟流式:句读处定稿(final),其余为 partial + const lastPunct = Math.max(sentence.lastIndexOf(',', i), sentence.lastIndexOf('。', i)); + setFinalText(sentence.slice(0, lastPunct + 1)); + setPartialText(sentence.slice(lastPunct + 1, i)); + }, 90); + }, []); + + React.useEffect(() => () => clearInterval(timerRef.current), []); + + const overlayState = phase === 'streaming' && (finalText || partialText) ? 'text' : 'listening'; + + return ( +
+
+ +
+
+ {docText || 光标在这里 — 在任意应用中按住快捷键即可语音输入} + +
+
+
+ + {phase !== 'idle' && ( +
+ +
+ )} +
+ +
+ + {docText && ( + + )} +
+
+ ); +} + +Object.assign(window, { OverlayDemo }); diff --git a/design/ui_kits/desktop/README.md b/design/ui_kits/desktop/README.md new file mode 100644 index 0000000..8808f56 --- /dev/null +++ b/design/ui_kits/desktop/README.md @@ -0,0 +1,27 @@ +# dudu 桌面端 UI Kit + +桌面端(Tauri 2,Mac / Windows)四个界面的高保真交互演示,对应 MVP 设计方案 §8.1: + +| Tab | 界面 | 交互 | +|---|---|---| +| 识别浮层 | 仿真备忘录窗口 + RecognitionOverlay | 按住按钮模拟 ⌘⇧Space:聆听中 → partial 流式 → 松开 final 注入 | +| 托盘菜单 | 常驻菜单栏下拉 | hover 高亮(accent 反白)| +| 设置 | 单页设置窗口 | Switch / 麦克风下拉 / 外观切换(驱动整页 Light/Dark)| +| 登录 / 购买 | 微信扫码 → 选时长包 → 支付 → 完成 | 点击走完整流程 | + +右上角月亮/太阳切换 Light / Dark;设置中"外观"同样生效(含跟随系统)。 + +## 设计要点 + +- 浮层:`--overlay-bg` 深色玻璃(两种模式不变),淡入 + 上移 4px(`--dur-base`),final 亮 / partial 灰。 +- 一屏一个蓝色主按钮;托盘 hover 用 accent 反白是系统菜单惯例。 +- 设置窗口宽 440,行高紧凑(`--control-sm` 控件),分组标题 12px。 +- 二维码为占位图案(`FakeQr`),接入后端 `/v1/auth/qr` 后替换。 + +## 文件 + +- `index.html` — 入口(tabs + 主题状态) +- `DesktopShared.jsx` — Lucide 图标子集 · DuduLogo · MacWindow · FakeQr +- `OverlayDemo.jsx` — push-to-talk 全流程模拟 +- `SettingsTray.jsx` — TrayMenu + SettingsWindow +- `LoginPurchase.jsx` — 登录/时长包/支付/完成 四步 diff --git a/design/ui_kits/desktop/SettingsTray.jsx b/design/ui_kits/desktop/SettingsTray.jsx new file mode 100644 index 0000000..ab7b9d3 --- /dev/null +++ b/design/ui_kits/desktop/SettingsTray.jsx @@ -0,0 +1,140 @@ +// dudu 桌面端 · 系统托盘菜单 + 设置窗口 + +function TrayMenu() { + const { Badge } = window.DuduDesignSystem_2e0172; + const Item = ({ children, right, accent, onClick }) => { + const [hover, setHover] = React.useState(false); + return ( +
setHover(true)} onMouseLeave={() => setHover(false)} onClick={onClick} + style={{ + display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 18, + padding: '7px 14px', cursor: 'default', fontSize: 'var(--text-sm)', + background: hover ? 'var(--accent)' : 'transparent', + color: hover ? 'var(--text-on-accent)' : (accent ? 'var(--text-1)' : 'var(--text-1)'), + transition: 'background var(--dur-fast) var(--ease-out)', + }}> + {children} + {right && {right}} +
+ ); + }; + const Sep = () =>
; + return ( +
+
+ + dudu 语音输入 + + 已就绪 +
+ + }>启用快捷键 + 按住说话 + 设置… + 时长余额 + + 账号 wang*** + 检查更新 + + 退出 dudu +
+ ); +} + +function SettingsWindow({ theme, onThemeChange }) { + const { Switch, SettingRow, HotkeyCombo, Button, Badge, ProgressBar } = window.DuduDesignSystem_2e0172; + const [autostart, setAutostart] = React.useState(true); + const [sound, setSound] = React.useState(false); + const [mic, setMic] = React.useState('MacBook Pro 麦克风'); + + const Section = ({ title, children }) => ( +
+
{title}
+ {children} +
+ ); + + const themeOptions = [['system', '跟随系统'], ['light', '浅色'], ['dark', '深色']]; + + return ( + +
+ + + + + + + + + + + + + + + +
+ +
+ +
+ + + + + + {themeOptions.map(([val, lab]) => ( + + ))} + + +
+ +
+ +
+ + + wang*** 余额充足 + + + + + 472 分钟 + + + +
+
+ 今日免费试用1 / 3 分钟 +
+ +
+
+
+ ); +} + +Object.assign(window, { TrayMenu, SettingsWindow }); diff --git a/design/ui_kits/desktop/index.html b/design/ui_kits/desktop/index.html new file mode 100644 index 0000000..b3231cb --- /dev/null +++ b/design/ui_kits/desktop/index.html @@ -0,0 +1,110 @@ + + + + + + +dudu 桌面端 UI Kit + + + + + + + + +
+ + + + + + + diff --git a/design/ui_kits/mobile/AppScreens.jsx b/design/ui_kits/mobile/AppScreens.jsx new file mode 100644 index 0000000..b84e0ec --- /dev/null +++ b/design/ui_kits/mobile/AppScreens.jsx @@ -0,0 +1,187 @@ +// dudu 移动端 · 主 App 四屏:启用引导 / 登录 / 购买时长 / 我的 + +function MAppHeader({ title }) { + return ( +
{title}
+ ); +} + +/* ---------- ① 启用引导 ---------- */ +function OnboardingScreen() { + const { Button } = window.DuduDesignSystem_2e0172; + const [step, setStep] = React.useState(0); + const steps = [ + { title: '添加键盘', desc: '设置 → 通用 → 键盘 → 添加新键盘,选择 dudu', icon: }, + { title: '开启完全访问', desc: '语音识别需要联网,请允许完全访问', icon: }, + { title: '试一试', desc: '在下面的输入框按住麦克风说一句话', icon: }, + ]; + return ( +
+
+ +
三步开启 dudu
+
按住说话,松开上屏
+
+ +
+ {steps.map((s, i) => { + const state = i < step ? 'done' : i === step ? 'active' : 'todo'; + return ( +
+ {state === 'done' ? : s.icon} + + {i + 1}. {s.title} + {s.desc} + +
+ ); + })} +
+ +
+ +
+
+ ); +} + +/* ---------- ② 登录 ---------- */ +function LoginScreen() { + return ( +
+
+ +
dudu
+
大模型语音输入 · 说话即上屏
+
+ +
+ 登录即同意《用户协议》与《隐私政策》 +
+
+ ); +} + +/* ---------- ③ 购买时长 ---------- */ +function PaywallScreen() { + const { Button } = window.DuduDesignSystem_2e0172; + const [pack, setPack] = React.useState('m'); + const PACKS = [ + ['s', '100 分钟', 9, '约 ¥0.09 / 分钟', ''], + ['m', '500 分钟', 39, '约 ¥0.078 / 分钟', '省 13%'], + ['l', '2000 分钟', 129, '约 ¥0.065 / 分钟', '省 28%'], + ]; + const price = PACKS.find((p) => p[0] === pack)[2]; + return ( +
+ + + {/* 余额卡 */} +
+ + 时长余额 + 472 分钟 + + 时长不过期
每天另有 3 分钟免费试用
+
+ + {/* 时长包 */} +
+ {PACKS.map(([key, lab, p, desc, tag]) => { + const active = pack === key; + return ( + + ); + })} +
+ +
+ +
支付后立即到账 · 时长不过期
+
+
+ ); +} + +/* ---------- ④ 我的 ---------- */ +function ProfileScreen() { + const { Badge, ProgressBar } = window.DuduDesignSystem_2e0172; + const perms = [ + { label: '麦克风权限', ok: true }, + { label: '键盘已添加', ok: true }, + { label: '完全访问', ok: false, hint: '去开启' }, + ]; + return ( +
+ +
+ + + + + wang*** + 时长余额 472 分钟 · 不过期 + + 余额充足 +
+ +
+
+ 今日免费试用1 / 3 分钟 +
+ +
+ +
+ {perms.map((p, i) => ( +
+ {p.label} + {p.ok ? ( + + ) : ( + + {p.hint} + + )} +
+ ))} +
+
+ ); +} + +Object.assign(window, { OnboardingScreen, LoginScreen, PaywallScreen, ProfileScreen }); diff --git a/design/ui_kits/mobile/KeyboardScreen.jsx b/design/ui_kits/mobile/KeyboardScreen.jsx new file mode 100644 index 0000000..4eeb044 --- /dev/null +++ b/design/ui_kits/mobile/KeyboardScreen.jsx @@ -0,0 +1,140 @@ +// dudu 移动端 · 语音键盘(键盘扩展) +// 微信聊天上下文 + 以语音为主的键盘:工具条 / partial 文本条 / 功能键行 / 大麦克风条 +// 按住麦克风条:partial 流式显示 → 松开 final 上屏到宿主输入框 + +const KB_SENTENCES = [ + '好的,我马上把文件发给你,大概十分钟之内。', + '今晚的聚餐改到八点,地址我发你定位。', +]; + +function KeyboardScreen({ account = 'ok' }) { + // account: ok(有余额) | trial(试用中) | guest(未登录) | quota(今日试用已用完) + const { MicBar } = window.DuduDesignSystem_2e0172; + const [recording, setRecording] = React.useState(false); + const [finalText, setFinalText] = React.useState(''); + const [partialText, setPartialText] = React.useState(''); + const [inputText, setInputText] = React.useState(''); + const [seconds, setSeconds] = React.useState(0); + const [idx, setIdx] = React.useState(0); + const timers = React.useRef({}); + const ref = React.useRef({}); + ref.current = { finalText, partialText, idx }; + + const stop = () => { + clearInterval(timers.current.t); clearInterval(timers.current.s); + const { finalText: f, partialText: p, idx: i } = ref.current; + const committed = f + p; + if (committed) { setInputText((v) => v + committed); setIdx((i + 1) % KB_SENTENCES.length); } + setFinalText(''); setPartialText(''); setSeconds(0); + setRecording(false); + }; + + const start = () => { + if (account === 'guest' || account === 'quota') return; + setRecording(true); setFinalText(''); setPartialText(''); setSeconds(0); + const sentence = KB_SENTENCES[ref.current.idx]; + let i = 0; + timers.current.t = setInterval(() => { + i += 1; + if (i >= sentence.length) { clearInterval(timers.current.t); setFinalText(sentence); setPartialText(''); return; } + const lastPunct = Math.max(sentence.lastIndexOf(',', i), sentence.lastIndexOf('。', i)); + setFinalText(sentence.slice(0, lastPunct + 1)); + setPartialText(sentence.slice(lastPunct + 1, i)); + }, 95); + timers.current.s = setInterval(() => setSeconds((s) => s + 1), 1000); + }; + React.useEffect(() => () => { clearInterval(timers.current.t); clearInterval(timers.current.s); }, []); + + const micState = account === 'guest' ? 'disabled' : account === 'quota' ? 'quota' : recording ? 'recording' : 'idle'; + const fmt = (s) => `00:${String(s).padStart(2, '0')}`; + + const FnKey = ({ children, label, flex = 1, onClick }) => ( + + ); + + return ( + + {/* 宿主 App:微信聊天 */} +
+
张三
+
+
+ 上次说的那份材料方便发我一下吗 +
+
+ 稍等,我看一下 +
+
+ {/* 宿主输入框 */} +
+
+ {inputText || '发消息…'} + +
+
+
+ + {/* ===== 键盘扩展 ===== */} +
+ {/* 工具条 */} +
+ + dudu 语音输入 + + + {account === 'guest' ? '未登录' + : account === 'quota' ? '今日试用已用完' + : recording ? fmt(seconds) + : account === 'trial' ? '试用 · 今日剩 2 分钟' + : 余额 472 分钟 } + +
+ + {/* partial 文本条(录音中替代功能键行) */} + {recording ? ( +
+ {finalText}{partialText} +
+ ) : ( +
+ + setInputText((v) => v + ',')}>, + setInputText((v) => v + '。')}>。 + setInputText((v) => v.slice(0, -1))}> + setInputText('')}> +
+ )} + + {/* 大麦克风条 */} +
+ recording && stop()} + /> +
+ {recording && ( +
+ 上滑取消 +
+ )} +
+
+ ); +} + +Object.assign(window, { KeyboardScreen }); diff --git a/design/ui_kits/mobile/MobileShared.jsx b/design/ui_kits/mobile/MobileShared.jsx new file mode 100644 index 0000000..db894cc --- /dev/null +++ b/design/ui_kits/mobile/MobileShared.jsx @@ -0,0 +1,103 @@ +// dudu 移动端 UI kit · 共享:Lucide 图标子集、DuduLogo、手机框 + +function DuMIcon({ size = 16, sw = 2, children, ...rest }) { + return ( + + ); +} + +const DuMIcons = { + Mic: (p) => ( + + + + + + ), + Globe: (p) => ( + + + + + + ), + Backspace: (p) => ( + + + + + + ), + Enter: (p) => ( + + + + + ), + Check: (p) => , + ChevronRight: (p) => , + User: (p) => ( + + + + + ), + Keyboard: (p) => ( + + + + + + + ), + Shield: (p) => ( + + + + ), + ArrowUp: (p) => , + Wechat: (p) => ( + /* 简化对话气泡(代微信图标占位,正式包内用官方资源)*/ + + + + ), +}; + +function DuduMLogo({ size = 20, sw = 3.5 }) { + return ( + + ); +} + +// 中性手机框:深色边框 + 打孔/刘海,内容区由调用方填充 +function PhoneFrame({ width = 312, height = 596, children }) { + return ( +
+
+ {/* 状态栏 */} +
+
+
+ {children} +
+
+ ); +} + +Object.assign(window, { DuMIcon, DuMIcons, DuduMLogo, PhoneFrame }); diff --git a/design/ui_kits/mobile/README.md b/design/ui_kits/mobile/README.md new file mode 100644 index 0000000..c161138 --- /dev/null +++ b/design/ui_kits/mobile/README.md @@ -0,0 +1,30 @@ +# dudu 移动端 UI Kit + +移动端(iOS / Android 原生键盘扩展 + 主 App)高保真交互演示,对应 MVP 设计方案 §8.2–8.3。 + +## 左:键盘扩展 + +微信聊天宿主上下文 + 以语音为主的键盘: + +- 工具条(dudu 标识 + 余额/试用/计时状态) +- 功能键行:切换键盘 / ,/ 。/ 退格 / 发送(语音为主,不做 26 键) +- 大麦克风条(`MicBar`,56px 胶囊):**按住说话** → partial 文本条流式刷新 → **松开** final 上屏到宿主输入框;录音中显示"上滑取消" +- 顶部状态 chips 切换四态:有余额 / 试用中(每天免费 3 分钟)/ 未登录(按键置灰)/ 试用已用完(橙色引导购买时长) + +## 右:主 App 四屏 + +- **启用引导**:三步卡片(添加键盘 → 完全访问 → 试一试),点主按钮推进 +- **登录**:logo + 微信一键登录(微信品牌绿 #07C160) +- **购买时长**:时长余额卡 + 时长包(100/500/2000 分钟,不过期)+ 微信支付 +- **我的**:账号卡(时长余额)+ 今日免费试用 ProgressBar + 权限自检(异常红色"去开启") + +右上角切换 Light / Dark。键盘面板与 App 均随主题;键帽用 `--surface-card` + `--shadow-key` 底边。 + +## 文件 + +- `index.html` — 入口(两台手机并排 + 状态 chips + 主题) +- `MobileShared.jsx` — Lucide 图标子集 · DuduMLogo · PhoneFrame +- `KeyboardScreen.jsx` — 键盘扩展全态 +- `AppScreens.jsx` — Onboarding / Login / Paywall / Profile + +注:微信图标为简化气泡占位,正式包请使用微信官方资源。 diff --git a/design/ui_kits/mobile/index.html b/design/ui_kits/mobile/index.html new file mode 100644 index 0000000..4ef985d --- /dev/null +++ b/design/ui_kits/mobile/index.html @@ -0,0 +1,131 @@ + + + + + + +dudu 移动端 UI Kit + + + + + + + + +
+ + + + + + diff --git a/desktop/feedback.html b/desktop/feedback.html new file mode 100644 index 0000000..c652f33 --- /dev/null +++ b/desktop/feedback.html @@ -0,0 +1,12 @@ + + + + + + 反馈问题 + + +
+ + + diff --git a/desktop/index.html b/desktop/index.html new file mode 100644 index 0000000..1290cb4 --- /dev/null +++ b/desktop/index.html @@ -0,0 +1,12 @@ + + + + + + dudu 设置 + + +
+ + + diff --git a/desktop/login.html b/desktop/login.html new file mode 100644 index 0000000..e88e0d1 --- /dev/null +++ b/desktop/login.html @@ -0,0 +1,11 @@ + + + + + dudu — 登录 + + +
+ + + diff --git a/desktop/onboarding.html b/desktop/onboarding.html new file mode 100644 index 0000000..2397d55 --- /dev/null +++ b/desktop/onboarding.html @@ -0,0 +1,12 @@ + + + + + + 欢迎使用 dudu + + +
+ + + diff --git a/desktop/overlay.html b/desktop/overlay.html new file mode 100644 index 0000000..59a95fa --- /dev/null +++ b/desktop/overlay.html @@ -0,0 +1,12 @@ + + + + + dudu + + + +
+ + + diff --git a/desktop/package-lock.json b/desktop/package-lock.json new file mode 100644 index 0000000..8b6dad3 --- /dev/null +++ b/desktop/package-lock.json @@ -0,0 +1,2036 @@ +{ + "name": "dudu-desktop", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "dudu-desktop", + "version": "0.1.0", + "dependencies": { + "@tauri-apps/api": "^2.4.0", + "@tauri-apps/plugin-global-shortcut": "^2.2.0", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@tauri-apps/cli": "^2.4.0", + "@vitejs/plugin-react": "^4.3.4", + "vite": "^6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "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-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.61.1.tgz", + "integrity": "sha512-JnBB8MdXj45cajvTuO5FmPlvFVJRQgvrz1uSEl3NwqFnReAPGwb8EanbGi4z2nRaqLzjJSv5/JmycoTKlRZxHA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.61.1.tgz", + "integrity": "sha512-Jx2g7iSjw4AOT0HDPHM9RV3GNjRXwybWtSFZiZAYUTjUwjVrYIwq3kBf+LnhqJlzXFAqTAh2F7IGI+O568exPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.61.1.tgz", + "integrity": "sha512-0F1L/Z3Eqv8mT2n3dCpeO8GcTvHvVqkP5/t6DMsn0KzhYVcg+s7Ncl5DS8qjKYEeio6Az0Gt6nyBORay5qIlCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.61.1.tgz", + "integrity": "sha512-qLttcH871ujY4YcVfUSShhOw+CsoTatYz8gRbHO7Bb92QH059/P0y5do1KMs41fY0BpD2x4AJH/gID0zFiqVKQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.61.1.tgz", + "integrity": "sha512-fUI4RapGE0Oh3mb8mgfvC1O2nU1RpDZUKnDQm3xB1Ipg7C2wTs5Kstz7G2uWK99a8S2yTMq8/P4uycwNa0nJyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.61.1.tgz", + "integrity": "sha512-H5YrdvJaDtI/U9/emrD4b++xkvp3y/JvOe4rizHbxvkyMfRS/CiRYdji+Pl8D0brEaNFWUh1drQxgAGIl6Xudw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.61.1.tgz", + "integrity": "sha512-Q8CBCCQtDFrYtXoeUXSrnFXKOnyUhx6bz+SkL6A0E7V8kAiCJ5pamq1WtbfpVGhR5TSpXY6ak3avmDc5fHTyJA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.61.1.tgz", + "integrity": "sha512-nwnhk1581l0FBVellGcVCAT0Oi06onEA3WB53sf01VO3I0UPBkMH9sXONYME2K0ovXcNayJfNtHfm6mpJElatQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.61.1.tgz", + "integrity": "sha512-x5Xr49hwt3hdW75UOZm3395YwwzPyauktslv29KpWL/T+vVAzoT3azLcTWv0eMciBNrx+DYjH4paehHoLpPvpg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.61.1.tgz", + "integrity": "sha512-unMS3H73DpaoPyyEVPjGKleM/s0mkmsauTENpw4INQY8y4+IuLNjkueQ5QCtC0D3N38Y38yhAU8OoZ20S2Tm6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.61.1.tgz", + "integrity": "sha512-zNZzGRnAhwjFEYmvphJRV5XaQGjs62cCmeYYHUT//NbvEnHauw+I85nGG+SiVg5ld4GX8D1IbKIX+ozITQnhMQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.61.1.tgz", + "integrity": "sha512-LdpWGL8X209B2SIvWjqlc8VZgM6PKfontSerGepuldQmHYrAOtnMCXeJkxXGbC+PPZVOuu5czJo7fNV6aeW8rQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.61.1.tgz", + "integrity": "sha512-EC5kTtNaNGOmbMGqar8dvJy6y/hg99GAwjfBz++pxZhQATXGcRjd6c5en5wcbru0vkRmiMGsQKdMJOOf6sza4g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.61.1.tgz", + "integrity": "sha512-8hiwp6D4acEcNK78I4rP0/XtS1sknWIAMJBPdR4l6zUtyTm5KiTDr5bXmWt4foY7nAN7AThDHgkLIEZOWKbzWw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.61.1.tgz", + "integrity": "sha512-10dh/h/BqA7DuMPWSxkR8uks18FRwnwOEqr5zOTEl+NOwP/OMzKX8OFR/Of9xxDA7D5qef1Nzar5WDD2kCCr1g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.61.1.tgz", + "integrity": "sha512-YKJ5lg35DP17gcAOggnihe+APw9HLyj1Xn7gsmGumBJAUDa6NGXNixJzmkWLhcK9TOuuyQjdamzvJefkO7qHZQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.61.1.tgz", + "integrity": "sha512-Mlil5G2Jj6a7B3LWGctg+XPL9vdXYuzCtNXfxOQ0nPjc2m6ueUktocPGH9bnAM0bNRKb/bAWTujUU7IJQdQA+g==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.61.1.tgz", + "integrity": "sha512-bVWIOIk6pV01p4CdUbPP7CJ/434z+OooYjDuFcR+44N35YvKUC66G8MGnvcWx5mWKW3g61J+t74l3Kj15Kwn2Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.61.1.tgz", + "integrity": "sha512-qy5pBvZbqNFheBz61R1rzsezjm0J7O2oNGoWtGoY89SZYLUfxAJTBAqDChqAIdB4rCiIbi9nF7yZ83GnNiLwSw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.61.1.tgz", + "integrity": "sha512-E83TXjI4zm0+5f2qO+UOudaCYIhYwpJ5jq6YCZNIZ+6CbfhKrkAGezeiASBL9ElxAxFsRS9ZhESv8mfnj6TKeg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.61.1.tgz", + "integrity": "sha512-fbWnKqVkjrJN38vNe3ahkbk6iejS/3b0Nt7EEtPpE6RBacZcGXNKbzfHN3GUUlXOPghUg0j6XUGrtjX9z1sIvA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.61.1.tgz", + "integrity": "sha512-ArMl38iVAbk0New1ogihQNY6iphLi4ZaRsa037gUzv5yeKPY8TD3Dmy4x2RNC1VztU/uqm+G+/RwFrSka3Oy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.61.1.tgz", + "integrity": "sha512-0mYtjHS9ucAbcATycCNK9IGBk/cCe/ma7EmSLGZdsxnOA8cjRIyU04wDpVAD9NiOfLUR9KTxdiO53uOkherqjQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.61.1.tgz", + "integrity": "sha512-gK1iCEPfpoSG9wfBihXxvBMi8ZfcWffYkEsC/Eih+iFENTaewvNcrEQ69lIOWYO5pePHKLHHO7nq5AILGO/HQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.61.1.tgz", + "integrity": "sha512-X+zaP2x+j4RXGfbp/seSoRHWnPxzApilDszisZxbYH5C/jTxFhCtDNdPGZb9lJyYPs24wGxruPF7Y+sIXt9Gzw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tauri-apps/api": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.11.0.tgz", + "integrity": "sha512-7CinYODhky9lmO23xHnUFv0Xt43fbtWMyxZcLcRBlFkcgXKuEirBvHpmtJ89YMhyeGcq20Wuc47Fa4XjyniywA==", + "license": "Apache-2.0 OR MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + } + }, + "node_modules/@tauri-apps/cli": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.11.2.tgz", + "integrity": "sha512-bk3HemqvGRoy+5D/dVMUQHKMYLglD0jVnMm/0iGMH6ufZ+p8r14m6BpIixwij3PBvZdvORUp1YifTD8QxVZ1Nw==", + "dev": true, + "license": "Apache-2.0 OR MIT", + "bin": { + "tauri": "tauri.js" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + }, + "optionalDependencies": { + "@tauri-apps/cli-darwin-arm64": "2.11.2", + "@tauri-apps/cli-darwin-x64": "2.11.2", + "@tauri-apps/cli-linux-arm-gnueabihf": "2.11.2", + "@tauri-apps/cli-linux-arm64-gnu": "2.11.2", + "@tauri-apps/cli-linux-arm64-musl": "2.11.2", + "@tauri-apps/cli-linux-riscv64-gnu": "2.11.2", + "@tauri-apps/cli-linux-x64-gnu": "2.11.2", + "@tauri-apps/cli-linux-x64-musl": "2.11.2", + "@tauri-apps/cli-win32-arm64-msvc": "2.11.2", + "@tauri-apps/cli-win32-ia32-msvc": "2.11.2", + "@tauri-apps/cli-win32-x64-msvc": "2.11.2" + } + }, + "node_modules/@tauri-apps/cli-darwin-arm64": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.11.2.tgz", + "integrity": "sha512-+4UZzLt+eOAEQCwgd+TqKgyUJMrvx+BgdXLLaqJYmPqzP+nE6YZr/hY6CWLYGQb8jFn99jEkmC6uA3tNvamA1w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-darwin-x64": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.11.2.tgz", + "integrity": "sha512-VjYYtZUPqDMLutSfJEyxFE3Bz+DPi7c8wC3imckgvciLDZLq4qwKJxBicg0BXGhXjJsl8vKWgWRFNMPELQ+Xyg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.11.2.tgz", + "integrity": "sha512-yMemD6f4i95AQriS8EazyOFzbE34yjnP16i3IOzpHGQvBoy2DjypFMFBq0NtPuITURv/cOGguRtHR5d79/9CSA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-gnu": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.11.2.tgz", + "integrity": "sha512-cgI91D2wL8GSgoWwZXDqt+DwnuZCP2/bz03QAE4TrhgAKIsrB4hX26W/H1EONPUUNkqrsgeCD0wU6pcNjV/5kw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-musl": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.11.2.tgz", + "integrity": "sha512-X1rm0BERqAAggtYTESSgXrS3sz4Sb/OiPiz54UqISlXW+GkR3vNIGnsy/lejNmoXGVqri3Q53BCfQiclOIyRPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-riscv64-gnu": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.11.2.tgz", + "integrity": "sha512-usbMLJbT3KtkOrBMDVeGYNM35aTHXx38SJSzTMSqqjeUIOQ+iVPjb2yAGNAE+KqmBbAx4FOFIyMeKXx2M/JKGQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-gnu": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.11.2.tgz", + "integrity": "sha512-Ru4gwJKPG0ctVGchRGpRup4Y4lW2SSfFnrbQcyHhCliKy4g8Qz97TrUgCur4CbWyAgKxvGh3SjrkA0LDYzDGiw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-musl": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.11.2.tgz", + "integrity": "sha512-eUm7T6clN1MMmNSRQ9gaWsQdyehQx2Gmn5hht/QUlqZQI/qcP2OJK5dnaxqwFzCr2HdsEo9ydxaqcS1oJzMvUw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-arm64-msvc": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.11.2.tgz", + "integrity": "sha512-HeeZW80jU+gVTOEX4X/hC6NVSAdDVXajwP5fxIZ/3z9WvUC7qrudX2GMTilYq6Dg0e0sk0XgsAJD1hZ5wPBXUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-ia32-msvc": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.11.2.tgz", + "integrity": "sha512-YhjQNZcXfbkCLyazSv1nPnJ9iRFE1wm6kc51FDbU10/Dk09io+6PAGMLjkxnX2GdM0qMnDmTjstY8mTDVvtKeA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-x64-msvc": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.11.2.tgz", + "integrity": "sha512-d2JchlFIpZevZVReyqhQOekJmb1UH3rhZ5VX6sH3ty9ETE0TKQavpihvoScUXfKKpW6HZC0MrFGRU0ZtD+w3gA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/plugin-global-shortcut": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-global-shortcut/-/plugin-global-shortcut-2.3.2.tgz", + "integrity": "sha512-UReHNXrLvpEjylE4jb4oCYiy96uRykPUthoCQCmRXYrd5hs5X9DrW+qOn7GLW57EJN4tdK8bgK5twBTz2NOxzA==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.11.0" + } + }, + "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.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.35", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.35.tgz", + "integrity": "sha512-honAfLBde0HAFLdNyBEfuuENkF6zR+ozxqxa/2zJKHBe1qzLqyTSeRKpdPEHAP03rlDGyQOPnCSxnVpVqQo9Mg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "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.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001799", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", + "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==", + "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/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/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/electron-to-chromium": { + "version": "1.5.371", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.371.tgz", + "integrity": "sha512-e9htk9mAYL6AzmkEhSvVVw7IWGSBJ/Bqdn2eRyRLrj1g6sncN4WbFt5qnILYoCktktr45pyjIrOiRvBThQ808w==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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/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/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/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/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/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "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/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.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "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/node-releases": { + "version": "2.0.47", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz", + "integrity": "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "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.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "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.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.61.1.tgz", + "integrity": "sha512-I4KW6iuRpuu2uHBLraZ1wNZe0DP7lnRha+VJ9tNaYVaVgKhW0aI3h4RYnoRPeql0flHm/Co55b7snEDcOfOJrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.61.1", + "@rollup/rollup-android-arm64": "4.61.1", + "@rollup/rollup-darwin-arm64": "4.61.1", + "@rollup/rollup-darwin-x64": "4.61.1", + "@rollup/rollup-freebsd-arm64": "4.61.1", + "@rollup/rollup-freebsd-x64": "4.61.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.61.1", + "@rollup/rollup-linux-arm-musleabihf": "4.61.1", + "@rollup/rollup-linux-arm64-gnu": "4.61.1", + "@rollup/rollup-linux-arm64-musl": "4.61.1", + "@rollup/rollup-linux-loong64-gnu": "4.61.1", + "@rollup/rollup-linux-loong64-musl": "4.61.1", + "@rollup/rollup-linux-ppc64-gnu": "4.61.1", + "@rollup/rollup-linux-ppc64-musl": "4.61.1", + "@rollup/rollup-linux-riscv64-gnu": "4.61.1", + "@rollup/rollup-linux-riscv64-musl": "4.61.1", + "@rollup/rollup-linux-s390x-gnu": "4.61.1", + "@rollup/rollup-linux-x64-gnu": "4.61.1", + "@rollup/rollup-linux-x64-musl": "4.61.1", + "@rollup/rollup-openbsd-x64": "4.61.1", + "@rollup/rollup-openharmony-arm64": "4.61.1", + "@rollup/rollup-win32-arm64-msvc": "4.61.1", + "@rollup/rollup-win32-ia32-msvc": "4.61.1", + "@rollup/rollup-win32-x64-gnu": "4.61.1", + "@rollup/rollup-win32-x64-msvc": "4.61.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "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/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/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "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/vite": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "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/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/desktop/package.json b/desktop/package.json new file mode 100644 index 0000000..c12fef3 --- /dev/null +++ b/desktop/package.json @@ -0,0 +1,22 @@ +{ + "name": "dudu-desktop", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "tauri": "tauri" + }, + "dependencies": { + "@tauri-apps/api": "^2.4.0", + "@tauri-apps/plugin-global-shortcut": "^2.2.0", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@tauri-apps/cli": "^2.4.0", + "@vitejs/plugin-react": "^4.3.4", + "vite": "^6.0.0" + } +} diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock new file mode 100644 index 0000000..5524b2c --- /dev/null +++ b/desktop/src-tauri/Cargo.lock @@ -0,0 +1,6019 @@ +# 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 = "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 = "alsa" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43" +dependencies = [ + "alsa-sys", + "bitflags 2.13.0", + "cfg-if", + "libc", +] + +[[package]] +name = "alsa-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527" +dependencies = [ + "libc", + "pkg-config", +] + +[[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 = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "arboard" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" +dependencies = [ + "clipboard-win", + "image", + "log", + "objc2 0.6.4", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "parking_lot", + "percent-encoding", + "windows-sys 0.60.2", + "x11rb", +] + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[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 = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags 2.13.0", + "cexpr", + "clang-sys", + "itertools", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex 1.3.0", + "syn 2.0.117", +] + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +dependencies = [ + "serde_core", +] + +[[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 = "block-sys" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae85a0696e7ea3b835a453750bf002770776609115e6d25c6d2ff28a8200f7e7" +dependencies = [ + "objc-sys", +] + +[[package]] +name = "block2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e58aa60e59d8dbfcc36138f5f18be5f24394d33b38b24f7fd0b1caa33095f22f" +dependencies = [ + "block-sys", + "objc2 0.5.2", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2 0.6.4", +] + +[[package]] +name = "brotli" +version = "8.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8119e4516436f5708bbc474a9d395bf12f1b5395e93a92a56e647ac3388c8610" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5962523e1b92ce1b5e793d9169b9943eece10d39f62550bc04bb605d75b94924" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[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.13.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.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex 2.0.1", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[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 = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link 0.2.1", +] + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading 0.8.9", +] + +[[package]] +name = "clipboard-win" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" +dependencies = [ + "error-code", +] + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[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.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "foreign-types 0.5.0", + "libc", +] + +[[package]] +name = "core-graphics" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" +dependencies = [ + "bitflags 2.13.0", + "core-foundation 0.10.1", + "core-graphics-types 0.2.0", + "foreign-types 0.5.0", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "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.13.0", + "core-foundation 0.10.1", + "libc", +] + +[[package]] +name = "coreaudio-rs" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "321077172d79c662f64f5071a03120748d5bb652f5231570141be24cfcd2bace" +dependencies = [ + "bitflags 1.3.2", + "core-foundation-sys", + "coreaudio-sys", +] + +[[package]] +name = "coreaudio-sys" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9b4739a805a62757a83e5654fa3faabec0442666b263bb2287d5a8185bfd953" +dependencies = [ + "bindgen", +] + +[[package]] +name = "cpal" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779" +dependencies = [ + "alsa", + "core-foundation-sys", + "coreaudio-rs", + "dasp_sample", + "jni", + "js-sys", + "libc", + "mach2", + "ndk 0.8.0", + "ndk-context", + "oboe", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows 0.54.0", +] + +[[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-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.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf", + "smallvec", +] + +[[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.117", +] + +[[package]] +name = "ctor" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98" +dependencies = [ + "ctor-proc-macro", + "dtor", +] + +[[package]] +name = "ctor-proc-macro" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dasp_sample" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" + +[[package]] +name = "data-encoding" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" + +[[package]] +name = "dbus" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b942602992bb7acfd1f51c49811c58a610ef9181b6e66f3e519d79b540a3bf73" +dependencies = [ + "libc", + "libdbus-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", + "serde_core", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.117", +] + +[[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 = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.13.0", + "block2 0.6.2", + "libc", + "objc2 0.6.4", +] + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[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.117", +] + +[[package]] +name = "dom_query" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" +dependencies = [ + "bit-set", + "cssparser", + "foldhash 0.2.0", + "html5ever", + "precomputed-hash", + "selectors", + "tendril", +] + +[[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 = "dtor" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" + +[[package]] +name = "dudu-desktop" +version = "0.1.0" +dependencies = [ + "arboard", + "base64 0.22.1", + "cpal", + "enigo", + "env_logger", + "futures-util", + "log", + "parking_lot", + "reqwest 0.12.28", + "serde", + "serde_json", + "tauri", + "tauri-build", + "tauri-plugin-global-shortcut", + "tokio", + "tokio-tungstenite", + "uuid", +] + +[[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.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "embed-resource" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31a88c8d26de40ed18fe748c547845aa39de1db3afd958f8cb91579f3644bcb" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 1.1.2+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 = "enigo" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0087a01fc8591217447d28005379fb5a183683cc83f0a4707af28cc6603f70fb" +dependencies = [ + "core-graphics 0.23.2", + "foreign-types-shared 0.3.1", + "icrate", + "libc", + "log", + "objc2 0.5.2", + "windows 0.56.0", + "xkbcommon", + "xkeysym", +] + +[[package]] +name = "env_filter" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + +[[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.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +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 = "error-code" +version = "3.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "fax" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a" + +[[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 = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[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.117", +] + +[[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 = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[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.117", +] + +[[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 = "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 = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix", + "windows-link 0.2.1", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[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.13.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.117", +] + +[[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 = "global-hotkey" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c386b0a4a70cb2d39fffd74480f985b6f0bfbcb934b6a6b6b7e630e448f242e" +dependencies = [ + "crossbeam-channel", + "keyboard-types", + "objc2 0.6.4", + "objc2-app-kit", + "once_cell", + "serde", + "thiserror 2.0.18", + "windows-sys 0.59.0", + "x11rb", + "xkeysym", +] + +[[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.117", +] + +[[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 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[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.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" +dependencies = [ + "log", + "markup5ever", +] + +[[package]] +name = "http" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +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.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[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", + "tokio", + "tower-service", + "tracing", +] + +[[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 = "icrate" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb69199826926eb864697bddd27f73d9fddcffc004f5733131e15b465e30642" +dependencies = [ + "block2 0.4.0", + "objc2 0.5.2", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "moxcms", + "num-traits", + "png 0.18.1", + "tiff", +] + +[[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.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.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 = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[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 = "jiff" +version = "0.2.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4603d3033e49e2b0e31229fcab20a5d40089c607d975cd9c80551dc69eed9102" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-static" +version = "0.2.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "782d32378dddf207193ac91cefb848ad41abb58195c95168e1291227a0832b47" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[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 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[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.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2025f20d7a4fa7785846e7b63d10a76d3f1cee98ee5cb79ea59703f95e42162" +dependencies = [ + "cfg-if", + "futures-util", + "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.13.0", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[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 0.7.4", + "once_cell", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libdbus-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +dependencies = [ + "pkg-config", +] + +[[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 = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link 0.2.1", +] + +[[package]] +name = "libredox" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" +dependencies = [ + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[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.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "mach2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" +dependencies = [ + "libc", +] + +[[package]] +name = "markup5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" +dependencies = [ + "log", + "tendril", + "web_atoms", +] + +[[package]] +name = "memchr" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" + +[[package]] +name = "memmap2" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a5a03cefb0d953ec0be133036f14e109412fa594edc2f77227249db66cc3ed" +dependencies = [ + "libc", +] + +[[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 = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[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.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "muda" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47a2e3dff89cd322c66647942668faee0a2b1f88ea6cbb4d374b4a8d7e92528c" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "objc2 0.6.4", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.18", + "windows-sys 0.61.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.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7" +dependencies = [ + "bitflags 2.13.0", + "jni-sys 0.3.1", + "log", + "ndk-sys 0.5.0+25.2.9519653", + "num_enum", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.13.0", + "jni-sys 0.3.1", + "log", + "ndk-sys 0.6.0+11769913", + "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.5.0+25.2.9519653" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[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.117", +] + +[[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.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +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.13.0", + "block2 0.6.2", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.13.0", + "objc2 0.6.4", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "objc2 0.6.4", + "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.13.0", + "dispatch2", + "objc2 0.6.4", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.13.0", + "dispatch2", + "objc2 0.6.4", + "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 0.6.4", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "objc2 0.6.4", + "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.13.0", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[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.13.0", + "block2 0.6.2", + "objc2 0.6.4", + "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.13.0", + "objc2 0.6.4", + "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.13.0", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-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.13.0", + "block2 0.6.2", + "objc2 0.6.4", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-core-text", + "objc2-foundation", + "objc2-quartz-core", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "objc2 0.6.4", + "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.13.0", + "block2 0.6.2", + "objc2 0.6.4", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "oboe" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb" +dependencies = [ + "jni", + "ndk 0.8.0", + "ndk-context", + "num-derive", + "num-traits", + "oboe-sys", +] + +[[package]] +name = "oboe-sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8bb09a4a2b1d668170cfe0a7d5bc103f8999fb316c98099b6a9939c9f2e79d" +dependencies = [ + "cc", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "openssl" +version = "0.10.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967" +dependencies = [ + "bitflags 2.13.0", + "cfg-if", + "foreign-types 0.3.2", + "libc", + "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.117", +] + +[[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.116" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28a22dc7140cda5f096e5e7724a6962ca81a7f8bfd2979f9b18c11af56318c4" +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 = "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 = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "plist" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092791278e026273c1b65bbdcfbba3a300f2994c896bd01ab01da613c29c46f1" +dependencies = [ + "base64 0.22.1", + "indexmap 2.14.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.13.0", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +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.117", +] + +[[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.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.12+spec-1.1.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-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pxfm" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f" + +[[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.39.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" +dependencies = [ + "memchr", +] + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.4", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +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 = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[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.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 = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.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.117", +] + +[[package]] +name = "regex" +version = "1.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" +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.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[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", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", +] + +[[package]] +name = "reqwest" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +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 = "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-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.13.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +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.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +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.117", +] + +[[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.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.13.0", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "selectors" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" +dependencies = [ + "bitflags 2.13.0", + "cssparser", + "derive_more", + "log", + "new_debug_unreachable", + "phf", + "phf_codegen", + "precomputed-hash", + "rustc-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +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.117", +] + +[[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.117", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +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.117", +] + +[[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.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +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.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" +dependencies = [ + "base64 0.22.1", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[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.117", +] + +[[package]] +name = "servo_arc" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[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 = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +dependencies = [ + "libc", + "windows-sys 0.61.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 0.9.0", + "objc2 0.6.4", + "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.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", +] + +[[package]] +name = "string_cache_codegen" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +dependencies = [ + "phf_generator", + "phf_shared", + "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", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +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.117", +] + +[[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.35.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9" +dependencies = [ + "bitflags 2.13.0", + "block2 0.6.2", + "core-foundation 0.10.1", + "core-graphics 0.25.0", + "crossbeam-channel", + "dbus", + "dispatch2", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni", + "libc", + "log", + "ndk 0.9.0", + "ndk-sys 0.6.0+11769913", + "objc2 0.6.4", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "once_cell", + "parking_lot", + "percent-encoding", + "raw-window-handle", + "tao-macros", + "unicode-segmentation", + "url", + "windows 0.61.3", + "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.117", +] + +[[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.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "437404997acf375d85f1177afa7e11bb971f274ed6a7b83a2a3e339015f4cc28" +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 0.6.4", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "percent-encoding", + "plist", + "raw-window-handle", + "reqwest 0.13.4", + "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 0.61.3", +] + +[[package]] +name = "tauri-build" +version = "2.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aa1f9055fc23919a54e4e125052bed16ed04aef0487086e758fe01a67b451c7" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs", + "glob", + "heck 0.5.0", + "json-patch", + "schemars 0.8.22", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a0319528a025a38c4078e7dae2c446f4e63620ddb0659a643ede1cb38f90e9" +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.117", + "tauri-utils", + "thiserror 2.0.18", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "2.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae6cb4e3896c21d2f6da5b31251d2faea0153bba56ed0e970f918115dbee4924" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-plugin" +version = "2.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e126abc9e84e35cdfd01596140a73a1850cdb0df0a23acf0185776c30b469a6e" +dependencies = [ + "anyhow", + "glob", + "plist", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri-utils", + "walkdir", +] + +[[package]] +name = "tauri-plugin-global-shortcut" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4dd9f4c5136c09cd962da0c86dc4accd4666db2ea591cf16e6597435843bd2b" +dependencies = [ + "global-hotkey", + "log", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", +] + +[[package]] +name = "tauri-runtime" +version = "2.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48222d7116c8807eaa6fe2f372e023fae125084e61e6eca6d70b7961cdf129ef" +dependencies = [ + "cookie", + "dpi", + "gtk", + "http", + "jni", + "objc2 0.6.4", + "objc2-ui-kit", + "objc2-web-kit", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webview2-com", + "windows 0.61.3", +] + +[[package]] +name = "tauri-runtime-wry" +version = "2.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b83849ee63ecb27a8e8d0fe51915ca215076914aca43f96db1179f0f415f6cd9" +dependencies = [ + "gtk", + "http", + "jni", + "log", + "objc2 0.6.4", + "objc2-app-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "url", + "webkit2gtk", + "webview2-com", + "windows 0.61.3", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092379df9a707631978e6c56b1bc2401d387f01e2d4a3c123360d167bbb9aa95" +dependencies = [ + "anyhow", + "brotli", + "cargo_metadata", + "ctor", + "dom_query", + "dunce", + "glob", + "http", + "infer", + "json-patch", + "log", + "memchr", + "phf", + "plist", + "proc-macro2", + "quote", + "regex", + "schemars 0.8.22", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "swift-rs", + "thiserror 2.0.18", + "toml 1.1.2+spec-1.1.0", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-winres" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6" +dependencies = [ + "dunce", + "embed-resource", + "toml 1.1.2+spec-1.1.0", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "tendril" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4790fc369d5a530f4b544b094e31388b9b3a37c0f4652ade4505945f5660d24" +dependencies = [ + "new_debug_unreachable", + "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.117", +] + +[[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.117", +] + +[[package]] +name = "tiff" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error", + "weezl", + "zune-jpeg", +] + +[[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[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-tungstenite" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9" +dependencies = [ + "futures-util", + "log", + "native-tls", + "tokio", + "tokio-native-tls", + "tungstenite", +] + +[[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.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.3", +] + +[[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_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +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.14.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.14.0", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.25.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.3", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.3", +] + +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + +[[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.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags 2.13.0", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[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.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15edbb0d80583e85ee8df283410038e17314df5cba30da2087a54a85216c0773" +dependencies = [ + "crossbeam-channel", + "dirs", + "libappindicator", + "muda", + "objc2 0.6.4", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.18", + "windows-sys 0.61.2", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tungstenite" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "native-tls", + "rand 0.8.6", + "sha1", + "thiserror 1.0.69", + "utf-8", +] + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[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 = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[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.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[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 = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "serde_core", + "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.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[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 0.51.0", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.123" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a254a4b10c19a76f09a27640e7ffbf9bc30bf67e16a3bf28aaefa4920fe81563" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54568702fabf5d4849ce2b90fadfa64168a097eaf4b351ce9df8b687a0086aaf" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.123" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24a40fc75b0ec6f3746ceb10d36f53a93dcd68a93b11b6445983945d79eba0dc" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.123" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "908f34bd9b9ce3d4caf07b72dfab63d61504d156856c6bd3cd87fa350cf3985b" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.123" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7acbf7616c27b194bbb550bf77ed0c2c3e5b7fd1260a93082b95fb7f47959b92" +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.14.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.13.0", + "hashbrown 0.15.5", + "indexmap 2.14.0", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e0871acf327f283dc6da28a1696cdc64fb355ba9f935d052021fa77f35cce69" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web_atoms" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7cff6eef815df1834fd250e3a2ff436044d82a9f1bc1980ca1dbdf07effc538" +dependencies = [ + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", +] + +[[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 = "webpki-roots" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +dependencies = [ + "rustls-pki-types", +] + +[[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 0.61.3", + "windows-core 0.61.2", + "windows-implement 0.60.2", + "windows-interface 0.59.3", +] + +[[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.117", +] + +[[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 0.61.3", + "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 0.6.4", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "windows-sys 0.59.0", + "windows-version", +] + +[[package]] +name = "windows" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" +dependencies = [ + "windows-core 0.54.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1de69df01bdf1ead2f4ac895dc77c9351aefff65b2f3db429a343f9cbf05e132" +dependencies = [ + "windows-core 0.56.0", + "windows-targets 0.52.6", +] + +[[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.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" +dependencies = [ + "windows-result 0.1.2", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4698e52ed2d08f8658ab0c39512a7c00ee5fe2688c65f8c0a4f06750d729f2a6" +dependencies = [ + "windows-implement 0.56.0", + "windows-interface 0.56.0", + "windows-result 0.1.2", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement 0.60.2", + "windows-interface 0.59.3", + "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 0.60.2", + "windows-interface 0.59.3", + "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.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6fc35f58ecd95a9b71c4f2329b911016e6bec66b3f2e6a4aad86bd2e99e2f9b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[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.117", +] + +[[package]] +name = "windows-interface" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08990546bf4edef8f431fa6326e032865f27138718c587dc21bc0265bbcb57cc" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[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.117", +] + +[[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-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets 0.52.6", +] + +[[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.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +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" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[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.14.0", + "prettyplease", + "syn 2.0.117", + "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.117", + "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.13.0", + "indexmap 2.14.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.14.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "wry" +version = "0.55.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514" +dependencies = [ + "base64 0.22.1", + "block2 0.6.2", + "cookie", + "crossbeam-channel", + "dirs", + "dom_query", + "dpi", + "dunce", + "gdkx11", + "gtk", + "http", + "javascriptcore-rs", + "jni", + "libc", + "ndk 0.9.0", + "objc2 0.6.4", + "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 0.61.3", + "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 = "x11rb" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "gethostname", + "rustix", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + +[[package]] +name = "xkbcommon" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13867d259930edc7091a6c41b4ce6eee464328c6ff9659b7e4c668ca20d4c91e" +dependencies = [ + "libc", + "memmap2", + "xkeysym", +] + +[[package]] +name = "xkeysym" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[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.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" + +[[package]] +name = "zune-jpeg" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" +dependencies = [ + "zune-core", +] diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml new file mode 100644 index 0000000..31d08c9 --- /dev/null +++ b/desktop/src-tauri/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "dudu-desktop" +version = "0.1.0" +description = "dudu 语音输入法桌面端" +edition = "2021" + +[lib] +name = "dudu_desktop_lib" +crate-type = ["staticlib", "cdylib", "rlib"] + +[build-dependencies] +tauri-build = { version = "2", features = [] } + +[dependencies] +tauri = { version = "2", features = ["tray-icon"] } +tauri-plugin-global-shortcut = "2" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "time"] } +tokio-tungstenite = { version = "0.24", features = ["native-tls"] } +futures-util = "0.3" +cpal = "0.15" +arboard = "3" +enigo = "0.2" +parking_lot = "0.12" +uuid = { version = "1", features = ["v4"] } +reqwest = { version = "0.12", features = ["json", "multipart", "rustls-tls"], default-features = false } +base64 = "0.22" +log = "0.4" +env_logger = "0.11" diff --git a/desktop/src-tauri/build.rs b/desktop/src-tauri/build.rs new file mode 100644 index 0000000..d860e1e --- /dev/null +++ b/desktop/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/desktop/src-tauri/capabilities/default.json b/desktop/src-tauri/capabilities/default.json new file mode 100644 index 0000000..489dee5 --- /dev/null +++ b/desktop/src-tauri/capabilities/default.json @@ -0,0 +1,7 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "default", + "description": "所有窗口的基础能力:核心 IPC(invoke/listen)+ 全局快捷键", + "windows": ["settings", "overlay", "tray", "login", "feedback", "onboarding"], + "permissions": ["core:default", "global-shortcut:default"] +} diff --git a/desktop/src-tauri/gen/schemas/acl-manifests.json b/desktop/src-tauri/gen/schemas/acl-manifests.json new file mode 100644 index 0000000..dc9a1f1 --- /dev/null +++ b/desktop/src-tauri/gen/schemas/acl-manifests.json @@ -0,0 +1 @@ +{"core":{"default_permission":{"identifier":"default","description":"Default core plugins set.","permissions":["core:path:default","core:event:default","core:window:default","core:webview:default","core:app:default","core:image:default","core:resources:default","core:menu:default","core:tray:default"]},"permissions":{},"permission_sets":{},"global_scope_schema":null},"core:app":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-version","allow-name","allow-tauri-version","allow-identifier","allow-bundle-type","allow-register-listener","allow-remove-listener","allow-supports-multiple-windows"]},"permissions":{"allow-app-hide":{"identifier":"allow-app-hide","description":"Enables the app_hide command without any pre-configured scope.","commands":{"allow":["app_hide"],"deny":[]}},"allow-app-show":{"identifier":"allow-app-show","description":"Enables the app_show command without any pre-configured scope.","commands":{"allow":["app_show"],"deny":[]}},"allow-bundle-type":{"identifier":"allow-bundle-type","description":"Enables the bundle_type command without any pre-configured scope.","commands":{"allow":["bundle_type"],"deny":[]}},"allow-default-window-icon":{"identifier":"allow-default-window-icon","description":"Enables the default_window_icon command without any pre-configured scope.","commands":{"allow":["default_window_icon"],"deny":[]}},"allow-fetch-data-store-identifiers":{"identifier":"allow-fetch-data-store-identifiers","description":"Enables the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":["fetch_data_store_identifiers"],"deny":[]}},"allow-identifier":{"identifier":"allow-identifier","description":"Enables the identifier command without any pre-configured scope.","commands":{"allow":["identifier"],"deny":[]}},"allow-name":{"identifier":"allow-name","description":"Enables the name command without any pre-configured scope.","commands":{"allow":["name"],"deny":[]}},"allow-register-listener":{"identifier":"allow-register-listener","description":"Enables the register_listener command without any pre-configured scope.","commands":{"allow":["register_listener"],"deny":[]}},"allow-remove-data-store":{"identifier":"allow-remove-data-store","description":"Enables the remove_data_store command without any pre-configured scope.","commands":{"allow":["remove_data_store"],"deny":[]}},"allow-remove-listener":{"identifier":"allow-remove-listener","description":"Enables the remove_listener command without any pre-configured scope.","commands":{"allow":["remove_listener"],"deny":[]}},"allow-set-app-theme":{"identifier":"allow-set-app-theme","description":"Enables the set_app_theme command without any pre-configured scope.","commands":{"allow":["set_app_theme"],"deny":[]}},"allow-set-dock-visibility":{"identifier":"allow-set-dock-visibility","description":"Enables the set_dock_visibility command without any pre-configured scope.","commands":{"allow":["set_dock_visibility"],"deny":[]}},"allow-supports-multiple-windows":{"identifier":"allow-supports-multiple-windows","description":"Enables the supports_multiple_windows command without any pre-configured scope.","commands":{"allow":["supports_multiple_windows"],"deny":[]}},"allow-tauri-version":{"identifier":"allow-tauri-version","description":"Enables the tauri_version command without any pre-configured scope.","commands":{"allow":["tauri_version"],"deny":[]}},"allow-version":{"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]}},"deny-app-hide":{"identifier":"deny-app-hide","description":"Denies the app_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["app_hide"]}},"deny-app-show":{"identifier":"deny-app-show","description":"Denies the app_show command without any pre-configured scope.","commands":{"allow":[],"deny":["app_show"]}},"deny-bundle-type":{"identifier":"deny-bundle-type","description":"Denies the bundle_type command without any pre-configured scope.","commands":{"allow":[],"deny":["bundle_type"]}},"deny-default-window-icon":{"identifier":"deny-default-window-icon","description":"Denies the default_window_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["default_window_icon"]}},"deny-fetch-data-store-identifiers":{"identifier":"deny-fetch-data-store-identifiers","description":"Denies the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":[],"deny":["fetch_data_store_identifiers"]}},"deny-identifier":{"identifier":"deny-identifier","description":"Denies the identifier command without any pre-configured scope.","commands":{"allow":[],"deny":["identifier"]}},"deny-name":{"identifier":"deny-name","description":"Denies the name command without any pre-configured scope.","commands":{"allow":[],"deny":["name"]}},"deny-register-listener":{"identifier":"deny-register-listener","description":"Denies the register_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["register_listener"]}},"deny-remove-data-store":{"identifier":"deny-remove-data-store","description":"Denies the remove_data_store command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_data_store"]}},"deny-remove-listener":{"identifier":"deny-remove-listener","description":"Denies the remove_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_listener"]}},"deny-set-app-theme":{"identifier":"deny-set-app-theme","description":"Denies the set_app_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_app_theme"]}},"deny-set-dock-visibility":{"identifier":"deny-set-dock-visibility","description":"Denies the set_dock_visibility command without any pre-configured scope.","commands":{"allow":[],"deny":["set_dock_visibility"]}},"deny-supports-multiple-windows":{"identifier":"deny-supports-multiple-windows","description":"Denies the supports_multiple_windows command without any pre-configured scope.","commands":{"allow":[],"deny":["supports_multiple_windows"]}},"deny-tauri-version":{"identifier":"deny-tauri-version","description":"Denies the tauri_version command without any pre-configured scope.","commands":{"allow":[],"deny":["tauri_version"]}},"deny-version":{"identifier":"deny-version","description":"Denies the version command without any pre-configured scope.","commands":{"allow":[],"deny":["version"]}}},"permission_sets":{},"global_scope_schema":null},"core:event":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-listen","allow-unlisten","allow-emit","allow-emit-to"]},"permissions":{"allow-emit":{"identifier":"allow-emit","description":"Enables the emit command without any pre-configured scope.","commands":{"allow":["emit"],"deny":[]}},"allow-emit-to":{"identifier":"allow-emit-to","description":"Enables the emit_to command without any pre-configured scope.","commands":{"allow":["emit_to"],"deny":[]}},"allow-listen":{"identifier":"allow-listen","description":"Enables the listen command without any pre-configured scope.","commands":{"allow":["listen"],"deny":[]}},"allow-unlisten":{"identifier":"allow-unlisten","description":"Enables the unlisten command without any pre-configured scope.","commands":{"allow":["unlisten"],"deny":[]}},"deny-emit":{"identifier":"deny-emit","description":"Denies the emit command without any pre-configured scope.","commands":{"allow":[],"deny":["emit"]}},"deny-emit-to":{"identifier":"deny-emit-to","description":"Denies the emit_to command without any pre-configured scope.","commands":{"allow":[],"deny":["emit_to"]}},"deny-listen":{"identifier":"deny-listen","description":"Denies the listen command without any pre-configured scope.","commands":{"allow":[],"deny":["listen"]}},"deny-unlisten":{"identifier":"deny-unlisten","description":"Denies the unlisten command without any pre-configured scope.","commands":{"allow":[],"deny":["unlisten"]}}},"permission_sets":{},"global_scope_schema":null},"core:image":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-from-bytes","allow-from-path","allow-rgba","allow-size"]},"permissions":{"allow-from-bytes":{"identifier":"allow-from-bytes","description":"Enables the from_bytes command without any pre-configured scope.","commands":{"allow":["from_bytes"],"deny":[]}},"allow-from-path":{"identifier":"allow-from-path","description":"Enables the from_path command without any pre-configured scope.","commands":{"allow":["from_path"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-rgba":{"identifier":"allow-rgba","description":"Enables the rgba command without any pre-configured scope.","commands":{"allow":["rgba"],"deny":[]}},"allow-size":{"identifier":"allow-size","description":"Enables the size command without any pre-configured scope.","commands":{"allow":["size"],"deny":[]}},"deny-from-bytes":{"identifier":"deny-from-bytes","description":"Denies the from_bytes command without any pre-configured scope.","commands":{"allow":[],"deny":["from_bytes"]}},"deny-from-path":{"identifier":"deny-from-path","description":"Denies the from_path command without any pre-configured scope.","commands":{"allow":[],"deny":["from_path"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-rgba":{"identifier":"deny-rgba","description":"Denies the rgba command without any pre-configured scope.","commands":{"allow":[],"deny":["rgba"]}},"deny-size":{"identifier":"deny-size","description":"Denies the size command without any pre-configured scope.","commands":{"allow":[],"deny":["size"]}}},"permission_sets":{},"global_scope_schema":null},"core:menu":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-append","allow-prepend","allow-insert","allow-remove","allow-remove-at","allow-items","allow-get","allow-popup","allow-create-default","allow-set-as-app-menu","allow-set-as-window-menu","allow-text","allow-set-text","allow-is-enabled","allow-set-enabled","allow-set-accelerator","allow-set-as-windows-menu-for-nsapp","allow-set-as-help-menu-for-nsapp","allow-is-checked","allow-set-checked","allow-set-icon"]},"permissions":{"allow-append":{"identifier":"allow-append","description":"Enables the append command without any pre-configured scope.","commands":{"allow":["append"],"deny":[]}},"allow-create-default":{"identifier":"allow-create-default","description":"Enables the create_default command without any pre-configured scope.","commands":{"allow":["create_default"],"deny":[]}},"allow-get":{"identifier":"allow-get","description":"Enables the get command without any pre-configured scope.","commands":{"allow":["get"],"deny":[]}},"allow-insert":{"identifier":"allow-insert","description":"Enables the insert command without any pre-configured scope.","commands":{"allow":["insert"],"deny":[]}},"allow-is-checked":{"identifier":"allow-is-checked","description":"Enables the is_checked command without any pre-configured scope.","commands":{"allow":["is_checked"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-items":{"identifier":"allow-items","description":"Enables the items command without any pre-configured scope.","commands":{"allow":["items"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-popup":{"identifier":"allow-popup","description":"Enables the popup command without any pre-configured scope.","commands":{"allow":["popup"],"deny":[]}},"allow-prepend":{"identifier":"allow-prepend","description":"Enables the prepend command without any pre-configured scope.","commands":{"allow":["prepend"],"deny":[]}},"allow-remove":{"identifier":"allow-remove","description":"Enables the remove command without any pre-configured scope.","commands":{"allow":["remove"],"deny":[]}},"allow-remove-at":{"identifier":"allow-remove-at","description":"Enables the remove_at command without any pre-configured scope.","commands":{"allow":["remove_at"],"deny":[]}},"allow-set-accelerator":{"identifier":"allow-set-accelerator","description":"Enables the set_accelerator command without any pre-configured scope.","commands":{"allow":["set_accelerator"],"deny":[]}},"allow-set-as-app-menu":{"identifier":"allow-set-as-app-menu","description":"Enables the set_as_app_menu command without any pre-configured scope.","commands":{"allow":["set_as_app_menu"],"deny":[]}},"allow-set-as-help-menu-for-nsapp":{"identifier":"allow-set-as-help-menu-for-nsapp","description":"Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_help_menu_for_nsapp"],"deny":[]}},"allow-set-as-window-menu":{"identifier":"allow-set-as-window-menu","description":"Enables the set_as_window_menu command without any pre-configured scope.","commands":{"allow":["set_as_window_menu"],"deny":[]}},"allow-set-as-windows-menu-for-nsapp":{"identifier":"allow-set-as-windows-menu-for-nsapp","description":"Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_windows_menu_for_nsapp"],"deny":[]}},"allow-set-checked":{"identifier":"allow-set-checked","description":"Enables the set_checked command without any pre-configured scope.","commands":{"allow":["set_checked"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-text":{"identifier":"allow-set-text","description":"Enables the set_text command without any pre-configured scope.","commands":{"allow":["set_text"],"deny":[]}},"allow-text":{"identifier":"allow-text","description":"Enables the text command without any pre-configured scope.","commands":{"allow":["text"],"deny":[]}},"deny-append":{"identifier":"deny-append","description":"Denies the append command without any pre-configured scope.","commands":{"allow":[],"deny":["append"]}},"deny-create-default":{"identifier":"deny-create-default","description":"Denies the create_default command without any pre-configured scope.","commands":{"allow":[],"deny":["create_default"]}},"deny-get":{"identifier":"deny-get","description":"Denies the get command without any pre-configured scope.","commands":{"allow":[],"deny":["get"]}},"deny-insert":{"identifier":"deny-insert","description":"Denies the insert command without any pre-configured scope.","commands":{"allow":[],"deny":["insert"]}},"deny-is-checked":{"identifier":"deny-is-checked","description":"Denies the is_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["is_checked"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-items":{"identifier":"deny-items","description":"Denies the items command without any pre-configured scope.","commands":{"allow":[],"deny":["items"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-popup":{"identifier":"deny-popup","description":"Denies the popup command without any pre-configured scope.","commands":{"allow":[],"deny":["popup"]}},"deny-prepend":{"identifier":"deny-prepend","description":"Denies the prepend command without any pre-configured scope.","commands":{"allow":[],"deny":["prepend"]}},"deny-remove":{"identifier":"deny-remove","description":"Denies the remove command without any pre-configured scope.","commands":{"allow":[],"deny":["remove"]}},"deny-remove-at":{"identifier":"deny-remove-at","description":"Denies the remove_at command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_at"]}},"deny-set-accelerator":{"identifier":"deny-set-accelerator","description":"Denies the set_accelerator command without any pre-configured scope.","commands":{"allow":[],"deny":["set_accelerator"]}},"deny-set-as-app-menu":{"identifier":"deny-set-as-app-menu","description":"Denies the set_as_app_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_app_menu"]}},"deny-set-as-help-menu-for-nsapp":{"identifier":"deny-set-as-help-menu-for-nsapp","description":"Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_help_menu_for_nsapp"]}},"deny-set-as-window-menu":{"identifier":"deny-set-as-window-menu","description":"Denies the set_as_window_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_window_menu"]}},"deny-set-as-windows-menu-for-nsapp":{"identifier":"deny-set-as-windows-menu-for-nsapp","description":"Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_windows_menu_for_nsapp"]}},"deny-set-checked":{"identifier":"deny-set-checked","description":"Denies the set_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["set_checked"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-text":{"identifier":"deny-set-text","description":"Denies the set_text command without any pre-configured scope.","commands":{"allow":[],"deny":["set_text"]}},"deny-text":{"identifier":"deny-text","description":"Denies the text command without any pre-configured scope.","commands":{"allow":[],"deny":["text"]}}},"permission_sets":{},"global_scope_schema":null},"core:path":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-resolve-directory","allow-resolve","allow-normalize","allow-join","allow-dirname","allow-extname","allow-basename","allow-is-absolute"]},"permissions":{"allow-basename":{"identifier":"allow-basename","description":"Enables the basename command without any pre-configured scope.","commands":{"allow":["basename"],"deny":[]}},"allow-dirname":{"identifier":"allow-dirname","description":"Enables the dirname command without any pre-configured scope.","commands":{"allow":["dirname"],"deny":[]}},"allow-extname":{"identifier":"allow-extname","description":"Enables the extname command without any pre-configured scope.","commands":{"allow":["extname"],"deny":[]}},"allow-is-absolute":{"identifier":"allow-is-absolute","description":"Enables the is_absolute command without any pre-configured scope.","commands":{"allow":["is_absolute"],"deny":[]}},"allow-join":{"identifier":"allow-join","description":"Enables the join command without any pre-configured scope.","commands":{"allow":["join"],"deny":[]}},"allow-normalize":{"identifier":"allow-normalize","description":"Enables the normalize command without any pre-configured scope.","commands":{"allow":["normalize"],"deny":[]}},"allow-resolve":{"identifier":"allow-resolve","description":"Enables the resolve command without any pre-configured scope.","commands":{"allow":["resolve"],"deny":[]}},"allow-resolve-directory":{"identifier":"allow-resolve-directory","description":"Enables the resolve_directory command without any pre-configured scope.","commands":{"allow":["resolve_directory"],"deny":[]}},"deny-basename":{"identifier":"deny-basename","description":"Denies the basename command without any pre-configured scope.","commands":{"allow":[],"deny":["basename"]}},"deny-dirname":{"identifier":"deny-dirname","description":"Denies the dirname command without any pre-configured scope.","commands":{"allow":[],"deny":["dirname"]}},"deny-extname":{"identifier":"deny-extname","description":"Denies the extname command without any pre-configured scope.","commands":{"allow":[],"deny":["extname"]}},"deny-is-absolute":{"identifier":"deny-is-absolute","description":"Denies the is_absolute command without any pre-configured scope.","commands":{"allow":[],"deny":["is_absolute"]}},"deny-join":{"identifier":"deny-join","description":"Denies the join command without any pre-configured scope.","commands":{"allow":[],"deny":["join"]}},"deny-normalize":{"identifier":"deny-normalize","description":"Denies the normalize command without any pre-configured scope.","commands":{"allow":[],"deny":["normalize"]}},"deny-resolve":{"identifier":"deny-resolve","description":"Denies the resolve command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve"]}},"deny-resolve-directory":{"identifier":"deny-resolve-directory","description":"Denies the resolve_directory command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve_directory"]}}},"permission_sets":{},"global_scope_schema":null},"core:resources":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-close"]},"permissions":{"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}}},"permission_sets":{},"global_scope_schema":null},"core:tray":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-get-by-id","allow-remove-by-id","allow-set-icon","allow-set-menu","allow-set-tooltip","allow-set-title","allow-set-visible","allow-set-temp-dir-path","allow-set-icon-as-template","allow-set-icon-with-as-template","allow-set-show-menu-on-left-click"]},"permissions":{"allow-get-by-id":{"identifier":"allow-get-by-id","description":"Enables the get_by_id command without any pre-configured scope.","commands":{"allow":["get_by_id"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-remove-by-id":{"identifier":"allow-remove-by-id","description":"Enables the remove_by_id command without any pre-configured scope.","commands":{"allow":["remove_by_id"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-icon-as-template":{"identifier":"allow-set-icon-as-template","description":"Enables the set_icon_as_template command without any pre-configured scope.","commands":{"allow":["set_icon_as_template"],"deny":[]}},"allow-set-icon-with-as-template":{"identifier":"allow-set-icon-with-as-template","description":"Enables the set_icon_with_as_template command without any pre-configured scope.","commands":{"allow":["set_icon_with_as_template"],"deny":[]}},"allow-set-menu":{"identifier":"allow-set-menu","description":"Enables the set_menu command without any pre-configured scope.","commands":{"allow":["set_menu"],"deny":[]}},"allow-set-show-menu-on-left-click":{"identifier":"allow-set-show-menu-on-left-click","description":"Enables the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":["set_show_menu_on_left_click"],"deny":[]}},"allow-set-temp-dir-path":{"identifier":"allow-set-temp-dir-path","description":"Enables the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":["set_temp_dir_path"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-tooltip":{"identifier":"allow-set-tooltip","description":"Enables the set_tooltip command without any pre-configured scope.","commands":{"allow":["set_tooltip"],"deny":[]}},"allow-set-visible":{"identifier":"allow-set-visible","description":"Enables the set_visible command without any pre-configured scope.","commands":{"allow":["set_visible"],"deny":[]}},"deny-get-by-id":{"identifier":"deny-get-by-id","description":"Denies the get_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["get_by_id"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-remove-by-id":{"identifier":"deny-remove-by-id","description":"Denies the remove_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_by_id"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-icon-as-template":{"identifier":"deny-set-icon-as-template","description":"Denies the set_icon_as_template command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon_as_template"]}},"deny-set-icon-with-as-template":{"identifier":"deny-set-icon-with-as-template","description":"Denies the set_icon_with_as_template command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon_with_as_template"]}},"deny-set-menu":{"identifier":"deny-set-menu","description":"Denies the set_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_menu"]}},"deny-set-show-menu-on-left-click":{"identifier":"deny-set-show-menu-on-left-click","description":"Denies the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":[],"deny":["set_show_menu_on_left_click"]}},"deny-set-temp-dir-path":{"identifier":"deny-set-temp-dir-path","description":"Denies the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":[],"deny":["set_temp_dir_path"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-tooltip":{"identifier":"deny-set-tooltip","description":"Denies the set_tooltip command without any pre-configured scope.","commands":{"allow":[],"deny":["set_tooltip"]}},"deny-set-visible":{"identifier":"deny-set-visible","description":"Denies the set_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible"]}}},"permission_sets":{},"global_scope_schema":null},"core:webview":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-webviews","allow-webview-position","allow-webview-size","allow-internal-toggle-devtools"]},"permissions":{"allow-clear-all-browsing-data":{"identifier":"allow-clear-all-browsing-data","description":"Enables the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":["clear_all_browsing_data"],"deny":[]}},"allow-create-webview":{"identifier":"allow-create-webview","description":"Enables the create_webview command without any pre-configured scope.","commands":{"allow":["create_webview"],"deny":[]}},"allow-create-webview-window":{"identifier":"allow-create-webview-window","description":"Enables the create_webview_window command without any pre-configured scope.","commands":{"allow":["create_webview_window"],"deny":[]}},"allow-get-all-webviews":{"identifier":"allow-get-all-webviews","description":"Enables the get_all_webviews command without any pre-configured scope.","commands":{"allow":["get_all_webviews"],"deny":[]}},"allow-internal-toggle-devtools":{"identifier":"allow-internal-toggle-devtools","description":"Enables the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":["internal_toggle_devtools"],"deny":[]}},"allow-print":{"identifier":"allow-print","description":"Enables the print command without any pre-configured scope.","commands":{"allow":["print"],"deny":[]}},"allow-reparent":{"identifier":"allow-reparent","description":"Enables the reparent command without any pre-configured scope.","commands":{"allow":["reparent"],"deny":[]}},"allow-set-webview-auto-resize":{"identifier":"allow-set-webview-auto-resize","description":"Enables the set_webview_auto_resize command without any pre-configured scope.","commands":{"allow":["set_webview_auto_resize"],"deny":[]}},"allow-set-webview-background-color":{"identifier":"allow-set-webview-background-color","description":"Enables the set_webview_background_color command without any pre-configured scope.","commands":{"allow":["set_webview_background_color"],"deny":[]}},"allow-set-webview-focus":{"identifier":"allow-set-webview-focus","description":"Enables the set_webview_focus command without any pre-configured scope.","commands":{"allow":["set_webview_focus"],"deny":[]}},"allow-set-webview-position":{"identifier":"allow-set-webview-position","description":"Enables the set_webview_position command without any pre-configured scope.","commands":{"allow":["set_webview_position"],"deny":[]}},"allow-set-webview-size":{"identifier":"allow-set-webview-size","description":"Enables the set_webview_size command without any pre-configured scope.","commands":{"allow":["set_webview_size"],"deny":[]}},"allow-set-webview-zoom":{"identifier":"allow-set-webview-zoom","description":"Enables the set_webview_zoom command without any pre-configured scope.","commands":{"allow":["set_webview_zoom"],"deny":[]}},"allow-webview-close":{"identifier":"allow-webview-close","description":"Enables the webview_close command without any pre-configured scope.","commands":{"allow":["webview_close"],"deny":[]}},"allow-webview-hide":{"identifier":"allow-webview-hide","description":"Enables the webview_hide command without any pre-configured scope.","commands":{"allow":["webview_hide"],"deny":[]}},"allow-webview-position":{"identifier":"allow-webview-position","description":"Enables the webview_position command without any pre-configured scope.","commands":{"allow":["webview_position"],"deny":[]}},"allow-webview-show":{"identifier":"allow-webview-show","description":"Enables the webview_show command without any pre-configured scope.","commands":{"allow":["webview_show"],"deny":[]}},"allow-webview-size":{"identifier":"allow-webview-size","description":"Enables the webview_size command without any pre-configured scope.","commands":{"allow":["webview_size"],"deny":[]}},"deny-clear-all-browsing-data":{"identifier":"deny-clear-all-browsing-data","description":"Denies the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":[],"deny":["clear_all_browsing_data"]}},"deny-create-webview":{"identifier":"deny-create-webview","description":"Denies the create_webview command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview"]}},"deny-create-webview-window":{"identifier":"deny-create-webview-window","description":"Denies the create_webview_window command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview_window"]}},"deny-get-all-webviews":{"identifier":"deny-get-all-webviews","description":"Denies the get_all_webviews command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_webviews"]}},"deny-internal-toggle-devtools":{"identifier":"deny-internal-toggle-devtools","description":"Denies the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_devtools"]}},"deny-print":{"identifier":"deny-print","description":"Denies the print command without any pre-configured scope.","commands":{"allow":[],"deny":["print"]}},"deny-reparent":{"identifier":"deny-reparent","description":"Denies the reparent command without any pre-configured scope.","commands":{"allow":[],"deny":["reparent"]}},"deny-set-webview-auto-resize":{"identifier":"deny-set-webview-auto-resize","description":"Denies the set_webview_auto_resize command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_auto_resize"]}},"deny-set-webview-background-color":{"identifier":"deny-set-webview-background-color","description":"Denies the set_webview_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_background_color"]}},"deny-set-webview-focus":{"identifier":"deny-set-webview-focus","description":"Denies the set_webview_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_focus"]}},"deny-set-webview-position":{"identifier":"deny-set-webview-position","description":"Denies the set_webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_position"]}},"deny-set-webview-size":{"identifier":"deny-set-webview-size","description":"Denies the set_webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_size"]}},"deny-set-webview-zoom":{"identifier":"deny-set-webview-zoom","description":"Denies the set_webview_zoom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_zoom"]}},"deny-webview-close":{"identifier":"deny-webview-close","description":"Denies the webview_close command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_close"]}},"deny-webview-hide":{"identifier":"deny-webview-hide","description":"Denies the webview_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_hide"]}},"deny-webview-position":{"identifier":"deny-webview-position","description":"Denies the webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_position"]}},"deny-webview-show":{"identifier":"deny-webview-show","description":"Denies the webview_show command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_show"]}},"deny-webview-size":{"identifier":"deny-webview-size","description":"Denies the webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_size"]}}},"permission_sets":{},"global_scope_schema":null},"core:window":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-windows","allow-scale-factor","allow-inner-position","allow-outer-position","allow-inner-size","allow-outer-size","allow-is-fullscreen","allow-is-minimized","allow-is-maximized","allow-is-focused","allow-is-decorated","allow-is-resizable","allow-is-maximizable","allow-is-minimizable","allow-is-closable","allow-is-visible","allow-is-enabled","allow-title","allow-current-monitor","allow-primary-monitor","allow-monitor-from-point","allow-available-monitors","allow-cursor-position","allow-theme","allow-is-always-on-top","allow-activity-name","allow-scene-identifier","allow-internal-toggle-maximize"]},"permissions":{"allow-activity-name":{"identifier":"allow-activity-name","description":"Enables the activity_name command without any pre-configured scope.","commands":{"allow":["activity_name"],"deny":[]}},"allow-available-monitors":{"identifier":"allow-available-monitors","description":"Enables the available_monitors command without any pre-configured scope.","commands":{"allow":["available_monitors"],"deny":[]}},"allow-center":{"identifier":"allow-center","description":"Enables the center command without any pre-configured scope.","commands":{"allow":["center"],"deny":[]}},"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"allow-create":{"identifier":"allow-create","description":"Enables the create command without any pre-configured scope.","commands":{"allow":["create"],"deny":[]}},"allow-current-monitor":{"identifier":"allow-current-monitor","description":"Enables the current_monitor command without any pre-configured scope.","commands":{"allow":["current_monitor"],"deny":[]}},"allow-cursor-position":{"identifier":"allow-cursor-position","description":"Enables the cursor_position command without any pre-configured scope.","commands":{"allow":["cursor_position"],"deny":[]}},"allow-destroy":{"identifier":"allow-destroy","description":"Enables the destroy command without any pre-configured scope.","commands":{"allow":["destroy"],"deny":[]}},"allow-get-all-windows":{"identifier":"allow-get-all-windows","description":"Enables the get_all_windows command without any pre-configured scope.","commands":{"allow":["get_all_windows"],"deny":[]}},"allow-hide":{"identifier":"allow-hide","description":"Enables the hide command without any pre-configured scope.","commands":{"allow":["hide"],"deny":[]}},"allow-inner-position":{"identifier":"allow-inner-position","description":"Enables the inner_position command without any pre-configured scope.","commands":{"allow":["inner_position"],"deny":[]}},"allow-inner-size":{"identifier":"allow-inner-size","description":"Enables the inner_size command without any pre-configured scope.","commands":{"allow":["inner_size"],"deny":[]}},"allow-internal-toggle-maximize":{"identifier":"allow-internal-toggle-maximize","description":"Enables the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":["internal_toggle_maximize"],"deny":[]}},"allow-is-always-on-top":{"identifier":"allow-is-always-on-top","description":"Enables the is_always_on_top command without any pre-configured scope.","commands":{"allow":["is_always_on_top"],"deny":[]}},"allow-is-closable":{"identifier":"allow-is-closable","description":"Enables the is_closable command without any pre-configured scope.","commands":{"allow":["is_closable"],"deny":[]}},"allow-is-decorated":{"identifier":"allow-is-decorated","description":"Enables the is_decorated command without any pre-configured scope.","commands":{"allow":["is_decorated"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-is-focused":{"identifier":"allow-is-focused","description":"Enables the is_focused command without any pre-configured scope.","commands":{"allow":["is_focused"],"deny":[]}},"allow-is-fullscreen":{"identifier":"allow-is-fullscreen","description":"Enables the is_fullscreen command without any pre-configured scope.","commands":{"allow":["is_fullscreen"],"deny":[]}},"allow-is-maximizable":{"identifier":"allow-is-maximizable","description":"Enables the is_maximizable command without any pre-configured scope.","commands":{"allow":["is_maximizable"],"deny":[]}},"allow-is-maximized":{"identifier":"allow-is-maximized","description":"Enables the is_maximized command without any pre-configured scope.","commands":{"allow":["is_maximized"],"deny":[]}},"allow-is-minimizable":{"identifier":"allow-is-minimizable","description":"Enables the is_minimizable command without any pre-configured scope.","commands":{"allow":["is_minimizable"],"deny":[]}},"allow-is-minimized":{"identifier":"allow-is-minimized","description":"Enables the is_minimized command without any pre-configured scope.","commands":{"allow":["is_minimized"],"deny":[]}},"allow-is-resizable":{"identifier":"allow-is-resizable","description":"Enables the is_resizable command without any pre-configured scope.","commands":{"allow":["is_resizable"],"deny":[]}},"allow-is-visible":{"identifier":"allow-is-visible","description":"Enables the is_visible command without any pre-configured scope.","commands":{"allow":["is_visible"],"deny":[]}},"allow-maximize":{"identifier":"allow-maximize","description":"Enables the maximize command without any pre-configured scope.","commands":{"allow":["maximize"],"deny":[]}},"allow-minimize":{"identifier":"allow-minimize","description":"Enables the minimize command without any pre-configured scope.","commands":{"allow":["minimize"],"deny":[]}},"allow-monitor-from-point":{"identifier":"allow-monitor-from-point","description":"Enables the monitor_from_point command without any pre-configured scope.","commands":{"allow":["monitor_from_point"],"deny":[]}},"allow-outer-position":{"identifier":"allow-outer-position","description":"Enables the outer_position command without any pre-configured scope.","commands":{"allow":["outer_position"],"deny":[]}},"allow-outer-size":{"identifier":"allow-outer-size","description":"Enables the outer_size command without any pre-configured scope.","commands":{"allow":["outer_size"],"deny":[]}},"allow-primary-monitor":{"identifier":"allow-primary-monitor","description":"Enables the primary_monitor command without any pre-configured scope.","commands":{"allow":["primary_monitor"],"deny":[]}},"allow-request-user-attention":{"identifier":"allow-request-user-attention","description":"Enables the request_user_attention command without any pre-configured scope.","commands":{"allow":["request_user_attention"],"deny":[]}},"allow-scale-factor":{"identifier":"allow-scale-factor","description":"Enables the scale_factor command without any pre-configured scope.","commands":{"allow":["scale_factor"],"deny":[]}},"allow-scene-identifier":{"identifier":"allow-scene-identifier","description":"Enables the scene_identifier command without any pre-configured scope.","commands":{"allow":["scene_identifier"],"deny":[]}},"allow-set-always-on-bottom":{"identifier":"allow-set-always-on-bottom","description":"Enables the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":["set_always_on_bottom"],"deny":[]}},"allow-set-always-on-top":{"identifier":"allow-set-always-on-top","description":"Enables the set_always_on_top command without any pre-configured scope.","commands":{"allow":["set_always_on_top"],"deny":[]}},"allow-set-background-color":{"identifier":"allow-set-background-color","description":"Enables the set_background_color command without any pre-configured scope.","commands":{"allow":["set_background_color"],"deny":[]}},"allow-set-badge-count":{"identifier":"allow-set-badge-count","description":"Enables the set_badge_count command without any pre-configured scope.","commands":{"allow":["set_badge_count"],"deny":[]}},"allow-set-badge-label":{"identifier":"allow-set-badge-label","description":"Enables the set_badge_label command without any pre-configured scope.","commands":{"allow":["set_badge_label"],"deny":[]}},"allow-set-closable":{"identifier":"allow-set-closable","description":"Enables the set_closable command without any pre-configured scope.","commands":{"allow":["set_closable"],"deny":[]}},"allow-set-content-protected":{"identifier":"allow-set-content-protected","description":"Enables the set_content_protected command without any pre-configured scope.","commands":{"allow":["set_content_protected"],"deny":[]}},"allow-set-cursor-grab":{"identifier":"allow-set-cursor-grab","description":"Enables the set_cursor_grab command without any pre-configured scope.","commands":{"allow":["set_cursor_grab"],"deny":[]}},"allow-set-cursor-icon":{"identifier":"allow-set-cursor-icon","description":"Enables the set_cursor_icon command without any pre-configured scope.","commands":{"allow":["set_cursor_icon"],"deny":[]}},"allow-set-cursor-position":{"identifier":"allow-set-cursor-position","description":"Enables the set_cursor_position command without any pre-configured scope.","commands":{"allow":["set_cursor_position"],"deny":[]}},"allow-set-cursor-visible":{"identifier":"allow-set-cursor-visible","description":"Enables the set_cursor_visible command without any pre-configured scope.","commands":{"allow":["set_cursor_visible"],"deny":[]}},"allow-set-decorations":{"identifier":"allow-set-decorations","description":"Enables the set_decorations command without any pre-configured scope.","commands":{"allow":["set_decorations"],"deny":[]}},"allow-set-effects":{"identifier":"allow-set-effects","description":"Enables the set_effects command without any pre-configured scope.","commands":{"allow":["set_effects"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-focus":{"identifier":"allow-set-focus","description":"Enables the set_focus command without any pre-configured scope.","commands":{"allow":["set_focus"],"deny":[]}},"allow-set-focusable":{"identifier":"allow-set-focusable","description":"Enables the set_focusable command without any pre-configured scope.","commands":{"allow":["set_focusable"],"deny":[]}},"allow-set-fullscreen":{"identifier":"allow-set-fullscreen","description":"Enables the set_fullscreen command without any pre-configured scope.","commands":{"allow":["set_fullscreen"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-ignore-cursor-events":{"identifier":"allow-set-ignore-cursor-events","description":"Enables the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":["set_ignore_cursor_events"],"deny":[]}},"allow-set-max-size":{"identifier":"allow-set-max-size","description":"Enables the set_max_size command without any pre-configured scope.","commands":{"allow":["set_max_size"],"deny":[]}},"allow-set-maximizable":{"identifier":"allow-set-maximizable","description":"Enables the set_maximizable command without any pre-configured scope.","commands":{"allow":["set_maximizable"],"deny":[]}},"allow-set-min-size":{"identifier":"allow-set-min-size","description":"Enables the set_min_size command without any pre-configured scope.","commands":{"allow":["set_min_size"],"deny":[]}},"allow-set-minimizable":{"identifier":"allow-set-minimizable","description":"Enables the set_minimizable command without any pre-configured scope.","commands":{"allow":["set_minimizable"],"deny":[]}},"allow-set-overlay-icon":{"identifier":"allow-set-overlay-icon","description":"Enables the set_overlay_icon command without any pre-configured scope.","commands":{"allow":["set_overlay_icon"],"deny":[]}},"allow-set-position":{"identifier":"allow-set-position","description":"Enables the set_position command without any pre-configured scope.","commands":{"allow":["set_position"],"deny":[]}},"allow-set-progress-bar":{"identifier":"allow-set-progress-bar","description":"Enables the set_progress_bar command without any pre-configured scope.","commands":{"allow":["set_progress_bar"],"deny":[]}},"allow-set-resizable":{"identifier":"allow-set-resizable","description":"Enables the set_resizable command without any pre-configured scope.","commands":{"allow":["set_resizable"],"deny":[]}},"allow-set-shadow":{"identifier":"allow-set-shadow","description":"Enables the set_shadow command without any pre-configured scope.","commands":{"allow":["set_shadow"],"deny":[]}},"allow-set-simple-fullscreen":{"identifier":"allow-set-simple-fullscreen","description":"Enables the set_simple_fullscreen command without any pre-configured scope.","commands":{"allow":["set_simple_fullscreen"],"deny":[]}},"allow-set-size":{"identifier":"allow-set-size","description":"Enables the set_size command without any pre-configured scope.","commands":{"allow":["set_size"],"deny":[]}},"allow-set-size-constraints":{"identifier":"allow-set-size-constraints","description":"Enables the set_size_constraints command without any pre-configured scope.","commands":{"allow":["set_size_constraints"],"deny":[]}},"allow-set-skip-taskbar":{"identifier":"allow-set-skip-taskbar","description":"Enables the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":["set_skip_taskbar"],"deny":[]}},"allow-set-theme":{"identifier":"allow-set-theme","description":"Enables the set_theme command without any pre-configured scope.","commands":{"allow":["set_theme"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-title-bar-style":{"identifier":"allow-set-title-bar-style","description":"Enables the set_title_bar_style command without any pre-configured scope.","commands":{"allow":["set_title_bar_style"],"deny":[]}},"allow-set-visible-on-all-workspaces":{"identifier":"allow-set-visible-on-all-workspaces","description":"Enables the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":["set_visible_on_all_workspaces"],"deny":[]}},"allow-show":{"identifier":"allow-show","description":"Enables the show command without any pre-configured scope.","commands":{"allow":["show"],"deny":[]}},"allow-start-dragging":{"identifier":"allow-start-dragging","description":"Enables the start_dragging command without any pre-configured scope.","commands":{"allow":["start_dragging"],"deny":[]}},"allow-start-resize-dragging":{"identifier":"allow-start-resize-dragging","description":"Enables the start_resize_dragging command without any pre-configured scope.","commands":{"allow":["start_resize_dragging"],"deny":[]}},"allow-theme":{"identifier":"allow-theme","description":"Enables the theme command without any pre-configured scope.","commands":{"allow":["theme"],"deny":[]}},"allow-title":{"identifier":"allow-title","description":"Enables the title command without any pre-configured scope.","commands":{"allow":["title"],"deny":[]}},"allow-toggle-maximize":{"identifier":"allow-toggle-maximize","description":"Enables the toggle_maximize command without any pre-configured scope.","commands":{"allow":["toggle_maximize"],"deny":[]}},"allow-unmaximize":{"identifier":"allow-unmaximize","description":"Enables the unmaximize command without any pre-configured scope.","commands":{"allow":["unmaximize"],"deny":[]}},"allow-unminimize":{"identifier":"allow-unminimize","description":"Enables the unminimize command without any pre-configured scope.","commands":{"allow":["unminimize"],"deny":[]}},"deny-activity-name":{"identifier":"deny-activity-name","description":"Denies the activity_name command without any pre-configured scope.","commands":{"allow":[],"deny":["activity_name"]}},"deny-available-monitors":{"identifier":"deny-available-monitors","description":"Denies the available_monitors command without any pre-configured scope.","commands":{"allow":[],"deny":["available_monitors"]}},"deny-center":{"identifier":"deny-center","description":"Denies the center command without any pre-configured scope.","commands":{"allow":[],"deny":["center"]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}},"deny-create":{"identifier":"deny-create","description":"Denies the create command without any pre-configured scope.","commands":{"allow":[],"deny":["create"]}},"deny-current-monitor":{"identifier":"deny-current-monitor","description":"Denies the current_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["current_monitor"]}},"deny-cursor-position":{"identifier":"deny-cursor-position","description":"Denies the cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["cursor_position"]}},"deny-destroy":{"identifier":"deny-destroy","description":"Denies the destroy command without any pre-configured scope.","commands":{"allow":[],"deny":["destroy"]}},"deny-get-all-windows":{"identifier":"deny-get-all-windows","description":"Denies the get_all_windows command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_windows"]}},"deny-hide":{"identifier":"deny-hide","description":"Denies the hide command without any pre-configured scope.","commands":{"allow":[],"deny":["hide"]}},"deny-inner-position":{"identifier":"deny-inner-position","description":"Denies the inner_position command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_position"]}},"deny-inner-size":{"identifier":"deny-inner-size","description":"Denies the inner_size command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_size"]}},"deny-internal-toggle-maximize":{"identifier":"deny-internal-toggle-maximize","description":"Denies the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_maximize"]}},"deny-is-always-on-top":{"identifier":"deny-is-always-on-top","description":"Denies the is_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["is_always_on_top"]}},"deny-is-closable":{"identifier":"deny-is-closable","description":"Denies the is_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_closable"]}},"deny-is-decorated":{"identifier":"deny-is-decorated","description":"Denies the is_decorated command without any pre-configured scope.","commands":{"allow":[],"deny":["is_decorated"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-is-focused":{"identifier":"deny-is-focused","description":"Denies the is_focused command without any pre-configured scope.","commands":{"allow":[],"deny":["is_focused"]}},"deny-is-fullscreen":{"identifier":"deny-is-fullscreen","description":"Denies the is_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["is_fullscreen"]}},"deny-is-maximizable":{"identifier":"deny-is-maximizable","description":"Denies the is_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximizable"]}},"deny-is-maximized":{"identifier":"deny-is-maximized","description":"Denies the is_maximized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximized"]}},"deny-is-minimizable":{"identifier":"deny-is-minimizable","description":"Denies the is_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimizable"]}},"deny-is-minimized":{"identifier":"deny-is-minimized","description":"Denies the is_minimized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimized"]}},"deny-is-resizable":{"identifier":"deny-is-resizable","description":"Denies the is_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_resizable"]}},"deny-is-visible":{"identifier":"deny-is-visible","description":"Denies the is_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["is_visible"]}},"deny-maximize":{"identifier":"deny-maximize","description":"Denies the maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["maximize"]}},"deny-minimize":{"identifier":"deny-minimize","description":"Denies the minimize command without any pre-configured scope.","commands":{"allow":[],"deny":["minimize"]}},"deny-monitor-from-point":{"identifier":"deny-monitor-from-point","description":"Denies the monitor_from_point command without any pre-configured scope.","commands":{"allow":[],"deny":["monitor_from_point"]}},"deny-outer-position":{"identifier":"deny-outer-position","description":"Denies the outer_position command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_position"]}},"deny-outer-size":{"identifier":"deny-outer-size","description":"Denies the outer_size command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_size"]}},"deny-primary-monitor":{"identifier":"deny-primary-monitor","description":"Denies the primary_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["primary_monitor"]}},"deny-request-user-attention":{"identifier":"deny-request-user-attention","description":"Denies the request_user_attention command without any pre-configured scope.","commands":{"allow":[],"deny":["request_user_attention"]}},"deny-scale-factor":{"identifier":"deny-scale-factor","description":"Denies the scale_factor command without any pre-configured scope.","commands":{"allow":[],"deny":["scale_factor"]}},"deny-scene-identifier":{"identifier":"deny-scene-identifier","description":"Denies the scene_identifier command without any pre-configured scope.","commands":{"allow":[],"deny":["scene_identifier"]}},"deny-set-always-on-bottom":{"identifier":"deny-set-always-on-bottom","description":"Denies the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_bottom"]}},"deny-set-always-on-top":{"identifier":"deny-set-always-on-top","description":"Denies the set_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_top"]}},"deny-set-background-color":{"identifier":"deny-set-background-color","description":"Denies the set_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_background_color"]}},"deny-set-badge-count":{"identifier":"deny-set-badge-count","description":"Denies the set_badge_count command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_count"]}},"deny-set-badge-label":{"identifier":"deny-set-badge-label","description":"Denies the set_badge_label command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_label"]}},"deny-set-closable":{"identifier":"deny-set-closable","description":"Denies the set_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_closable"]}},"deny-set-content-protected":{"identifier":"deny-set-content-protected","description":"Denies the set_content_protected command without any pre-configured scope.","commands":{"allow":[],"deny":["set_content_protected"]}},"deny-set-cursor-grab":{"identifier":"deny-set-cursor-grab","description":"Denies the set_cursor_grab command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_grab"]}},"deny-set-cursor-icon":{"identifier":"deny-set-cursor-icon","description":"Denies the set_cursor_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_icon"]}},"deny-set-cursor-position":{"identifier":"deny-set-cursor-position","description":"Denies the set_cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_position"]}},"deny-set-cursor-visible":{"identifier":"deny-set-cursor-visible","description":"Denies the set_cursor_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_visible"]}},"deny-set-decorations":{"identifier":"deny-set-decorations","description":"Denies the set_decorations command without any pre-configured scope.","commands":{"allow":[],"deny":["set_decorations"]}},"deny-set-effects":{"identifier":"deny-set-effects","description":"Denies the set_effects command without any pre-configured scope.","commands":{"allow":[],"deny":["set_effects"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-focus":{"identifier":"deny-set-focus","description":"Denies the set_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focus"]}},"deny-set-focusable":{"identifier":"deny-set-focusable","description":"Denies the set_focusable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focusable"]}},"deny-set-fullscreen":{"identifier":"deny-set-fullscreen","description":"Denies the set_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_fullscreen"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-ignore-cursor-events":{"identifier":"deny-set-ignore-cursor-events","description":"Denies the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":[],"deny":["set_ignore_cursor_events"]}},"deny-set-max-size":{"identifier":"deny-set-max-size","description":"Denies the set_max_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_max_size"]}},"deny-set-maximizable":{"identifier":"deny-set-maximizable","description":"Denies the set_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_maximizable"]}},"deny-set-min-size":{"identifier":"deny-set-min-size","description":"Denies the set_min_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_min_size"]}},"deny-set-minimizable":{"identifier":"deny-set-minimizable","description":"Denies the set_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_minimizable"]}},"deny-set-overlay-icon":{"identifier":"deny-set-overlay-icon","description":"Denies the set_overlay_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_overlay_icon"]}},"deny-set-position":{"identifier":"deny-set-position","description":"Denies the set_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_position"]}},"deny-set-progress-bar":{"identifier":"deny-set-progress-bar","description":"Denies the set_progress_bar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_progress_bar"]}},"deny-set-resizable":{"identifier":"deny-set-resizable","description":"Denies the set_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_resizable"]}},"deny-set-shadow":{"identifier":"deny-set-shadow","description":"Denies the set_shadow command without any pre-configured scope.","commands":{"allow":[],"deny":["set_shadow"]}},"deny-set-simple-fullscreen":{"identifier":"deny-set-simple-fullscreen","description":"Denies the set_simple_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_simple_fullscreen"]}},"deny-set-size":{"identifier":"deny-set-size","description":"Denies the set_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size"]}},"deny-set-size-constraints":{"identifier":"deny-set-size-constraints","description":"Denies the set_size_constraints command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size_constraints"]}},"deny-set-skip-taskbar":{"identifier":"deny-set-skip-taskbar","description":"Denies the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_skip_taskbar"]}},"deny-set-theme":{"identifier":"deny-set-theme","description":"Denies the set_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_theme"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-title-bar-style":{"identifier":"deny-set-title-bar-style","description":"Denies the set_title_bar_style command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title_bar_style"]}},"deny-set-visible-on-all-workspaces":{"identifier":"deny-set-visible-on-all-workspaces","description":"Denies the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible_on_all_workspaces"]}},"deny-show":{"identifier":"deny-show","description":"Denies the show command without any pre-configured scope.","commands":{"allow":[],"deny":["show"]}},"deny-start-dragging":{"identifier":"deny-start-dragging","description":"Denies the start_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_dragging"]}},"deny-start-resize-dragging":{"identifier":"deny-start-resize-dragging","description":"Denies the start_resize_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_resize_dragging"]}},"deny-theme":{"identifier":"deny-theme","description":"Denies the theme command without any pre-configured scope.","commands":{"allow":[],"deny":["theme"]}},"deny-title":{"identifier":"deny-title","description":"Denies the title command without any pre-configured scope.","commands":{"allow":[],"deny":["title"]}},"deny-toggle-maximize":{"identifier":"deny-toggle-maximize","description":"Denies the toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["toggle_maximize"]}},"deny-unmaximize":{"identifier":"deny-unmaximize","description":"Denies the unmaximize command without any pre-configured scope.","commands":{"allow":[],"deny":["unmaximize"]}},"deny-unminimize":{"identifier":"deny-unminimize","description":"Denies the unminimize command without any pre-configured scope.","commands":{"allow":[],"deny":["unminimize"]}}},"permission_sets":{},"global_scope_schema":null},"global-shortcut":{"default_permission":{"identifier":"default","description":"No features are enabled by default, as we believe\nthe shortcuts can be inherently dangerous and it is\napplication specific if specific shortcuts should be\nregistered or unregistered.\n","permissions":[]},"permissions":{"allow-is-registered":{"identifier":"allow-is-registered","description":"Enables the is_registered command without any pre-configured scope.","commands":{"allow":["is_registered"],"deny":[]}},"allow-register":{"identifier":"allow-register","description":"Enables the register command without any pre-configured scope.","commands":{"allow":["register"],"deny":[]}},"allow-register-all":{"identifier":"allow-register-all","description":"Enables the register_all command without any pre-configured scope.","commands":{"allow":["register_all"],"deny":[]}},"allow-unregister":{"identifier":"allow-unregister","description":"Enables the unregister command without any pre-configured scope.","commands":{"allow":["unregister"],"deny":[]}},"allow-unregister-all":{"identifier":"allow-unregister-all","description":"Enables the unregister_all command without any pre-configured scope.","commands":{"allow":["unregister_all"],"deny":[]}},"deny-is-registered":{"identifier":"deny-is-registered","description":"Denies the is_registered command without any pre-configured scope.","commands":{"allow":[],"deny":["is_registered"]}},"deny-register":{"identifier":"deny-register","description":"Denies the register command without any pre-configured scope.","commands":{"allow":[],"deny":["register"]}},"deny-register-all":{"identifier":"deny-register-all","description":"Denies the register_all command without any pre-configured scope.","commands":{"allow":[],"deny":["register_all"]}},"deny-unregister":{"identifier":"deny-unregister","description":"Denies the unregister command without any pre-configured scope.","commands":{"allow":[],"deny":["unregister"]}},"deny-unregister-all":{"identifier":"deny-unregister-all","description":"Denies the unregister_all command without any pre-configured scope.","commands":{"allow":[],"deny":["unregister_all"]}}},"permission_sets":{},"global_scope_schema":null}} \ No newline at end of file diff --git a/desktop/src-tauri/gen/schemas/capabilities.json b/desktop/src-tauri/gen/schemas/capabilities.json new file mode 100644 index 0000000..a292161 --- /dev/null +++ b/desktop/src-tauri/gen/schemas/capabilities.json @@ -0,0 +1 @@ +{"default":{"identifier":"default","description":"所有窗口的基础能力:核心 IPC(invoke/listen)+ 全局快捷键","local":true,"windows":["settings","overlay","tray","login","feedback","onboarding"],"permissions":["core:default","global-shortcut:default"]}} \ No newline at end of file diff --git a/desktop/src-tauri/gen/schemas/desktop-schema.json b/desktop/src-tauri/gen/schemas/desktop-schema.json new file mode 100644 index 0000000..3680773 --- /dev/null +++ b/desktop/src-tauri/gen/schemas/desktop-schema.json @@ -0,0 +1,2358 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "CapabilityFile", + "description": "Capability formats accepted in a capability file.", + "anyOf": [ + { + "description": "A single capability.", + "allOf": [ + { + "$ref": "#/definitions/Capability" + } + ] + }, + { + "description": "A list of capabilities.", + "type": "array", + "items": { + "$ref": "#/definitions/Capability" + } + }, + { + "description": "A list of capabilities.", + "type": "object", + "required": [ + "capabilities" + ], + "properties": { + "capabilities": { + "description": "The list of capabilities.", + "type": "array", + "items": { + "$ref": "#/definitions/Capability" + } + } + } + } + ], + "definitions": { + "Capability": { + "description": "A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\n\nIt controls application windows' and webviews' fine grained access to the Tauri core, application, or plugin commands. If a webview or its window is not matching any capability then it has no access to the IPC layer at all.\n\nThis can be done to create groups of windows, based on their required system access, which can reduce impact of frontend vulnerabilities in less privileged windows. Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`. A Window can have none, one, or multiple associated capabilities.\n\n## Example\n\n```json { \"identifier\": \"main-user-files-write\", \"description\": \"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.\", \"windows\": [ \"main\" ], \"permissions\": [ \"core:default\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] }, ], \"platforms\": [\"macOS\",\"windows\"] } ```", + "type": "object", + "required": [ + "identifier", + "permissions" + ], + "properties": { + "identifier": { + "description": "Identifier of the capability.\n\n## Example\n\n`main-user-files-write`", + "type": "string" + }, + "description": { + "description": "Description of what the capability is intended to allow on associated windows.\n\nIt should contain a description of what the grouped permissions should allow.\n\n## Example\n\nThis capability allows the `main` window access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.", + "default": "", + "type": "string" + }, + "remote": { + "description": "Configure remote URLs that can use the capability permissions.\n\nThis setting is optional and defaults to not being set, as our default use case is that the content is served from our local application.\n\n:::caution Make sure you understand the security implications of providing remote sources with local system access. :::\n\n## Example\n\n```json { \"urls\": [\"https://*.mydomain.dev\"] } ```", + "anyOf": [ + { + "$ref": "#/definitions/CapabilityRemote" + }, + { + "type": "null" + } + ] + }, + "local": { + "description": "Whether this capability is enabled for local app URLs or not. Defaults to `true`.", + "default": true, + "type": "boolean" + }, + "windows": { + "description": "List of windows that are affected by this capability. Can be a glob pattern.\n\nIf a window label matches any of the patterns in this list, the capability will be enabled on all the webviews of that window, regardless of the value of [`Self::webviews`].\n\nOn multiwebview windows, prefer specifying [`Self::webviews`] and omitting [`Self::windows`] for a fine grained access control.\n\n## Example\n\n`[\"main\"]`", + "type": "array", + "items": { + "type": "string" + } + }, + "webviews": { + "description": "List of webviews that are affected by this capability. Can be a glob pattern.\n\nThe capability will be enabled on all the webviews whose label matches any of the patterns in this list, regardless of whether the webview's window label matches a pattern in [`Self::windows`].\n\n## Example\n\n`[\"sub-webview-one\", \"sub-webview-two\"]`", + "type": "array", + "items": { + "type": "string" + } + }, + "permissions": { + "description": "List of permissions attached to this capability.\n\nMust include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`. For commands directly implemented in the application itself only `${permission-name}` is required.\n\n## Example\n\n```json [ \"core:default\", \"shell:allow-open\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] } ] ```", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionEntry" + }, + "uniqueItems": true + }, + "platforms": { + "description": "Limit which target platforms this capability applies to.\n\nBy default all platforms are targeted.\n\n## Example\n\n`[\"macOS\",\"windows\"]`", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Target" + } + } + } + }, + "CapabilityRemote": { + "description": "Configuration for remote URLs that are associated with the capability.", + "type": "object", + "required": [ + "urls" + ], + "properties": { + "urls": { + "description": "Remote domains this capability refers to using the [URLPattern standard](https://urlpattern.spec.whatwg.org/).\n\n## Examples\n\n- \"https://*.mydomain.dev\": allows subdomains of mydomain.dev - \"https://mydomain.dev/api/*\": allows any subpath of mydomain.dev/api", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "PermissionEntry": { + "description": "An entry for a permission value in a [`Capability`] can be either a raw permission [`Identifier`] or an object that references a permission and extends its scope.", + "anyOf": [ + { + "description": "Reference a permission or permission set by identifier.", + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ] + }, + { + "description": "Reference a permission or permission set by identifier and extends its scope.", + "type": "object", + "allOf": [ + { + "properties": { + "identifier": { + "description": "Identifier of the permission or permission set.", + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ] + }, + "allow": { + "description": "Data that defines what is allowed by the scope.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + }, + "deny": { + "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + } + } + } + ], + "required": [ + "identifier" + ] + } + ] + }, + "Identifier": { + "description": "Permission identifier", + "oneOf": [ + { + "description": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`", + "type": "string", + "const": "core:default", + "markdownDescription": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`" + }, + { + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-supports-multiple-windows`", + "type": "string", + "const": "core:app:default", + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-supports-multiple-windows`" + }, + { + "description": "Enables the app_hide command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-app-hide", + "markdownDescription": "Enables the app_hide command without any pre-configured scope." + }, + { + "description": "Enables the app_show command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-app-show", + "markdownDescription": "Enables the app_show command without any pre-configured scope." + }, + { + "description": "Enables the bundle_type command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-bundle-type", + "markdownDescription": "Enables the bundle_type command without any pre-configured scope." + }, + { + "description": "Enables the default_window_icon command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-default-window-icon", + "markdownDescription": "Enables the default_window_icon command without any pre-configured scope." + }, + { + "description": "Enables the fetch_data_store_identifiers command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-fetch-data-store-identifiers", + "markdownDescription": "Enables the fetch_data_store_identifiers command without any pre-configured scope." + }, + { + "description": "Enables the identifier command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-identifier", + "markdownDescription": "Enables the identifier command without any pre-configured scope." + }, + { + "description": "Enables the name command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-name", + "markdownDescription": "Enables the name command without any pre-configured scope." + }, + { + "description": "Enables the register_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-register-listener", + "markdownDescription": "Enables the register_listener command without any pre-configured scope." + }, + { + "description": "Enables the remove_data_store command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-remove-data-store", + "markdownDescription": "Enables the remove_data_store command without any pre-configured scope." + }, + { + "description": "Enables the remove_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-remove-listener", + "markdownDescription": "Enables the remove_listener command without any pre-configured scope." + }, + { + "description": "Enables the set_app_theme command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-set-app-theme", + "markdownDescription": "Enables the set_app_theme command without any pre-configured scope." + }, + { + "description": "Enables the set_dock_visibility command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-set-dock-visibility", + "markdownDescription": "Enables the set_dock_visibility command without any pre-configured scope." + }, + { + "description": "Enables the supports_multiple_windows command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-supports-multiple-windows", + "markdownDescription": "Enables the supports_multiple_windows command without any pre-configured scope." + }, + { + "description": "Enables the tauri_version command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-tauri-version", + "markdownDescription": "Enables the tauri_version command without any pre-configured scope." + }, + { + "description": "Enables the version command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-version", + "markdownDescription": "Enables the version command without any pre-configured scope." + }, + { + "description": "Denies the app_hide command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-app-hide", + "markdownDescription": "Denies the app_hide command without any pre-configured scope." + }, + { + "description": "Denies the app_show command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-app-show", + "markdownDescription": "Denies the app_show command without any pre-configured scope." + }, + { + "description": "Denies the bundle_type command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-bundle-type", + "markdownDescription": "Denies the bundle_type command without any pre-configured scope." + }, + { + "description": "Denies the default_window_icon command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-default-window-icon", + "markdownDescription": "Denies the default_window_icon command without any pre-configured scope." + }, + { + "description": "Denies the fetch_data_store_identifiers command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-fetch-data-store-identifiers", + "markdownDescription": "Denies the fetch_data_store_identifiers command without any pre-configured scope." + }, + { + "description": "Denies the identifier command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-identifier", + "markdownDescription": "Denies the identifier command without any pre-configured scope." + }, + { + "description": "Denies the name command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-name", + "markdownDescription": "Denies the name command without any pre-configured scope." + }, + { + "description": "Denies the register_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-register-listener", + "markdownDescription": "Denies the register_listener command without any pre-configured scope." + }, + { + "description": "Denies the remove_data_store command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-remove-data-store", + "markdownDescription": "Denies the remove_data_store command without any pre-configured scope." + }, + { + "description": "Denies the remove_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-remove-listener", + "markdownDescription": "Denies the remove_listener command without any pre-configured scope." + }, + { + "description": "Denies the set_app_theme command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-set-app-theme", + "markdownDescription": "Denies the set_app_theme command without any pre-configured scope." + }, + { + "description": "Denies the set_dock_visibility command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-set-dock-visibility", + "markdownDescription": "Denies the set_dock_visibility command without any pre-configured scope." + }, + { + "description": "Denies the supports_multiple_windows command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-supports-multiple-windows", + "markdownDescription": "Denies the supports_multiple_windows command without any pre-configured scope." + }, + { + "description": "Denies the tauri_version command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-tauri-version", + "markdownDescription": "Denies the tauri_version command without any pre-configured scope." + }, + { + "description": "Denies the version command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-version", + "markdownDescription": "Denies the version command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-listen`\n- `allow-unlisten`\n- `allow-emit`\n- `allow-emit-to`", + "type": "string", + "const": "core:event:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-listen`\n- `allow-unlisten`\n- `allow-emit`\n- `allow-emit-to`" + }, + { + "description": "Enables the emit command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-emit", + "markdownDescription": "Enables the emit command without any pre-configured scope." + }, + { + "description": "Enables the emit_to command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-emit-to", + "markdownDescription": "Enables the emit_to command without any pre-configured scope." + }, + { + "description": "Enables the listen command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-listen", + "markdownDescription": "Enables the listen command without any pre-configured scope." + }, + { + "description": "Enables the unlisten command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-unlisten", + "markdownDescription": "Enables the unlisten command without any pre-configured scope." + }, + { + "description": "Denies the emit command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-emit", + "markdownDescription": "Denies the emit command without any pre-configured scope." + }, + { + "description": "Denies the emit_to command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-emit-to", + "markdownDescription": "Denies the emit_to command without any pre-configured scope." + }, + { + "description": "Denies the listen command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-listen", + "markdownDescription": "Denies the listen command without any pre-configured scope." + }, + { + "description": "Denies the unlisten command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-unlisten", + "markdownDescription": "Denies the unlisten command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-from-bytes`\n- `allow-from-path`\n- `allow-rgba`\n- `allow-size`", + "type": "string", + "const": "core:image:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-from-bytes`\n- `allow-from-path`\n- `allow-rgba`\n- `allow-size`" + }, + { + "description": "Enables the from_bytes command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-from-bytes", + "markdownDescription": "Enables the from_bytes command without any pre-configured scope." + }, + { + "description": "Enables the from_path command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-from-path", + "markdownDescription": "Enables the from_path command without any pre-configured scope." + }, + { + "description": "Enables the new command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-new", + "markdownDescription": "Enables the new command without any pre-configured scope." + }, + { + "description": "Enables the rgba command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-rgba", + "markdownDescription": "Enables the rgba command without any pre-configured scope." + }, + { + "description": "Enables the size command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-size", + "markdownDescription": "Enables the size command without any pre-configured scope." + }, + { + "description": "Denies the from_bytes command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-from-bytes", + "markdownDescription": "Denies the from_bytes command without any pre-configured scope." + }, + { + "description": "Denies the from_path command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-from-path", + "markdownDescription": "Denies the from_path command without any pre-configured scope." + }, + { + "description": "Denies the new command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-new", + "markdownDescription": "Denies the new command without any pre-configured scope." + }, + { + "description": "Denies the rgba command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-rgba", + "markdownDescription": "Denies the rgba command without any pre-configured scope." + }, + { + "description": "Denies the size command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-size", + "markdownDescription": "Denies the size command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-append`\n- `allow-prepend`\n- `allow-insert`\n- `allow-remove`\n- `allow-remove-at`\n- `allow-items`\n- `allow-get`\n- `allow-popup`\n- `allow-create-default`\n- `allow-set-as-app-menu`\n- `allow-set-as-window-menu`\n- `allow-text`\n- `allow-set-text`\n- `allow-is-enabled`\n- `allow-set-enabled`\n- `allow-set-accelerator`\n- `allow-set-as-windows-menu-for-nsapp`\n- `allow-set-as-help-menu-for-nsapp`\n- `allow-is-checked`\n- `allow-set-checked`\n- `allow-set-icon`", + "type": "string", + "const": "core:menu:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-append`\n- `allow-prepend`\n- `allow-insert`\n- `allow-remove`\n- `allow-remove-at`\n- `allow-items`\n- `allow-get`\n- `allow-popup`\n- `allow-create-default`\n- `allow-set-as-app-menu`\n- `allow-set-as-window-menu`\n- `allow-text`\n- `allow-set-text`\n- `allow-is-enabled`\n- `allow-set-enabled`\n- `allow-set-accelerator`\n- `allow-set-as-windows-menu-for-nsapp`\n- `allow-set-as-help-menu-for-nsapp`\n- `allow-is-checked`\n- `allow-set-checked`\n- `allow-set-icon`" + }, + { + "description": "Enables the append command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-append", + "markdownDescription": "Enables the append command without any pre-configured scope." + }, + { + "description": "Enables the create_default command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-create-default", + "markdownDescription": "Enables the create_default command without any pre-configured scope." + }, + { + "description": "Enables the get command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-get", + "markdownDescription": "Enables the get command without any pre-configured scope." + }, + { + "description": "Enables the insert command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-insert", + "markdownDescription": "Enables the insert command without any pre-configured scope." + }, + { + "description": "Enables the is_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-is-checked", + "markdownDescription": "Enables the is_checked command without any pre-configured scope." + }, + { + "description": "Enables the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-is-enabled", + "markdownDescription": "Enables the is_enabled command without any pre-configured scope." + }, + { + "description": "Enables the items command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-items", + "markdownDescription": "Enables the items command without any pre-configured scope." + }, + { + "description": "Enables the new command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-new", + "markdownDescription": "Enables the new command without any pre-configured scope." + }, + { + "description": "Enables the popup command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-popup", + "markdownDescription": "Enables the popup command without any pre-configured scope." + }, + { + "description": "Enables the prepend command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-prepend", + "markdownDescription": "Enables the prepend command without any pre-configured scope." + }, + { + "description": "Enables the remove command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-remove", + "markdownDescription": "Enables the remove command without any pre-configured scope." + }, + { + "description": "Enables the remove_at command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-remove-at", + "markdownDescription": "Enables the remove_at command without any pre-configured scope." + }, + { + "description": "Enables the set_accelerator command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-accelerator", + "markdownDescription": "Enables the set_accelerator command without any pre-configured scope." + }, + { + "description": "Enables the set_as_app_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-app-menu", + "markdownDescription": "Enables the set_as_app_menu command without any pre-configured scope." + }, + { + "description": "Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-help-menu-for-nsapp", + "markdownDescription": "Enables the set_as_help_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Enables the set_as_window_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-window-menu", + "markdownDescription": "Enables the set_as_window_menu command without any pre-configured scope." + }, + { + "description": "Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-windows-menu-for-nsapp", + "markdownDescription": "Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Enables the set_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-checked", + "markdownDescription": "Enables the set_checked command without any pre-configured scope." + }, + { + "description": "Enables the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-enabled", + "markdownDescription": "Enables the set_enabled command without any pre-configured scope." + }, + { + "description": "Enables the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-icon", + "markdownDescription": "Enables the set_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-text", + "markdownDescription": "Enables the set_text command without any pre-configured scope." + }, + { + "description": "Enables the text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-text", + "markdownDescription": "Enables the text command without any pre-configured scope." + }, + { + "description": "Denies the append command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-append", + "markdownDescription": "Denies the append command without any pre-configured scope." + }, + { + "description": "Denies the create_default command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-create-default", + "markdownDescription": "Denies the create_default command without any pre-configured scope." + }, + { + "description": "Denies the get command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-get", + "markdownDescription": "Denies the get command without any pre-configured scope." + }, + { + "description": "Denies the insert command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-insert", + "markdownDescription": "Denies the insert command without any pre-configured scope." + }, + { + "description": "Denies the is_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-is-checked", + "markdownDescription": "Denies the is_checked command without any pre-configured scope." + }, + { + "description": "Denies the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-is-enabled", + "markdownDescription": "Denies the is_enabled command without any pre-configured scope." + }, + { + "description": "Denies the items command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-items", + "markdownDescription": "Denies the items command without any pre-configured scope." + }, + { + "description": "Denies the new command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-new", + "markdownDescription": "Denies the new command without any pre-configured scope." + }, + { + "description": "Denies the popup command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-popup", + "markdownDescription": "Denies the popup command without any pre-configured scope." + }, + { + "description": "Denies the prepend command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-prepend", + "markdownDescription": "Denies the prepend command without any pre-configured scope." + }, + { + "description": "Denies the remove command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-remove", + "markdownDescription": "Denies the remove command without any pre-configured scope." + }, + { + "description": "Denies the remove_at command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-remove-at", + "markdownDescription": "Denies the remove_at command without any pre-configured scope." + }, + { + "description": "Denies the set_accelerator command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-accelerator", + "markdownDescription": "Denies the set_accelerator command without any pre-configured scope." + }, + { + "description": "Denies the set_as_app_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-app-menu", + "markdownDescription": "Denies the set_as_app_menu command without any pre-configured scope." + }, + { + "description": "Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-help-menu-for-nsapp", + "markdownDescription": "Denies the set_as_help_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Denies the set_as_window_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-window-menu", + "markdownDescription": "Denies the set_as_window_menu command without any pre-configured scope." + }, + { + "description": "Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-windows-menu-for-nsapp", + "markdownDescription": "Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Denies the set_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-checked", + "markdownDescription": "Denies the set_checked command without any pre-configured scope." + }, + { + "description": "Denies the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-enabled", + "markdownDescription": "Denies the set_enabled command without any pre-configured scope." + }, + { + "description": "Denies the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-icon", + "markdownDescription": "Denies the set_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-text", + "markdownDescription": "Denies the set_text command without any pre-configured scope." + }, + { + "description": "Denies the text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-text", + "markdownDescription": "Denies the text command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-resolve-directory`\n- `allow-resolve`\n- `allow-normalize`\n- `allow-join`\n- `allow-dirname`\n- `allow-extname`\n- `allow-basename`\n- `allow-is-absolute`", + "type": "string", + "const": "core:path:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-resolve-directory`\n- `allow-resolve`\n- `allow-normalize`\n- `allow-join`\n- `allow-dirname`\n- `allow-extname`\n- `allow-basename`\n- `allow-is-absolute`" + }, + { + "description": "Enables the basename command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-basename", + "markdownDescription": "Enables the basename command without any pre-configured scope." + }, + { + "description": "Enables the dirname command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-dirname", + "markdownDescription": "Enables the dirname command without any pre-configured scope." + }, + { + "description": "Enables the extname command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-extname", + "markdownDescription": "Enables the extname command without any pre-configured scope." + }, + { + "description": "Enables the is_absolute command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-is-absolute", + "markdownDescription": "Enables the is_absolute command without any pre-configured scope." + }, + { + "description": "Enables the join command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-join", + "markdownDescription": "Enables the join command without any pre-configured scope." + }, + { + "description": "Enables the normalize command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-normalize", + "markdownDescription": "Enables the normalize command without any pre-configured scope." + }, + { + "description": "Enables the resolve command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-resolve", + "markdownDescription": "Enables the resolve command without any pre-configured scope." + }, + { + "description": "Enables the resolve_directory command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-resolve-directory", + "markdownDescription": "Enables the resolve_directory command without any pre-configured scope." + }, + { + "description": "Denies the basename command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-basename", + "markdownDescription": "Denies the basename command without any pre-configured scope." + }, + { + "description": "Denies the dirname command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-dirname", + "markdownDescription": "Denies the dirname command without any pre-configured scope." + }, + { + "description": "Denies the extname command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-extname", + "markdownDescription": "Denies the extname command without any pre-configured scope." + }, + { + "description": "Denies the is_absolute command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-is-absolute", + "markdownDescription": "Denies the is_absolute command without any pre-configured scope." + }, + { + "description": "Denies the join command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-join", + "markdownDescription": "Denies the join command without any pre-configured scope." + }, + { + "description": "Denies the normalize command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-normalize", + "markdownDescription": "Denies the normalize command without any pre-configured scope." + }, + { + "description": "Denies the resolve command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-resolve", + "markdownDescription": "Denies the resolve command without any pre-configured scope." + }, + { + "description": "Denies the resolve_directory command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-resolve-directory", + "markdownDescription": "Denies the resolve_directory command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-close`", + "type": "string", + "const": "core:resources:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-close`" + }, + { + "description": "Enables the close command without any pre-configured scope.", + "type": "string", + "const": "core:resources:allow-close", + "markdownDescription": "Enables the close command without any pre-configured scope." + }, + { + "description": "Denies the close command without any pre-configured scope.", + "type": "string", + "const": "core:resources:deny-close", + "markdownDescription": "Denies the close command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-icon-with-as-template`\n- `allow-set-show-menu-on-left-click`", + "type": "string", + "const": "core:tray:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-icon-with-as-template`\n- `allow-set-show-menu-on-left-click`" + }, + { + "description": "Enables the get_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-get-by-id", + "markdownDescription": "Enables the get_by_id command without any pre-configured scope." + }, + { + "description": "Enables the new command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-new", + "markdownDescription": "Enables the new command without any pre-configured scope." + }, + { + "description": "Enables the remove_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-remove-by-id", + "markdownDescription": "Enables the remove_by_id command without any pre-configured scope." + }, + { + "description": "Enables the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-icon", + "markdownDescription": "Enables the set_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_icon_as_template command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-icon-as-template", + "markdownDescription": "Enables the set_icon_as_template command without any pre-configured scope." + }, + { + "description": "Enables the set_icon_with_as_template command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-icon-with-as-template", + "markdownDescription": "Enables the set_icon_with_as_template command without any pre-configured scope." + }, + { + "description": "Enables the set_menu command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-menu", + "markdownDescription": "Enables the set_menu command without any pre-configured scope." + }, + { + "description": "Enables the set_show_menu_on_left_click command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-show-menu-on-left-click", + "markdownDescription": "Enables the set_show_menu_on_left_click command without any pre-configured scope." + }, + { + "description": "Enables the set_temp_dir_path command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-temp-dir-path", + "markdownDescription": "Enables the set_temp_dir_path command without any pre-configured scope." + }, + { + "description": "Enables the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-title", + "markdownDescription": "Enables the set_title command without any pre-configured scope." + }, + { + "description": "Enables the set_tooltip command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-tooltip", + "markdownDescription": "Enables the set_tooltip command without any pre-configured scope." + }, + { + "description": "Enables the set_visible command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-visible", + "markdownDescription": "Enables the set_visible command without any pre-configured scope." + }, + { + "description": "Denies the get_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-get-by-id", + "markdownDescription": "Denies the get_by_id command without any pre-configured scope." + }, + { + "description": "Denies the new command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-new", + "markdownDescription": "Denies the new command without any pre-configured scope." + }, + { + "description": "Denies the remove_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-remove-by-id", + "markdownDescription": "Denies the remove_by_id command without any pre-configured scope." + }, + { + "description": "Denies the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-icon", + "markdownDescription": "Denies the set_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_icon_as_template command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-icon-as-template", + "markdownDescription": "Denies the set_icon_as_template command without any pre-configured scope." + }, + { + "description": "Denies the set_icon_with_as_template command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-icon-with-as-template", + "markdownDescription": "Denies the set_icon_with_as_template command without any pre-configured scope." + }, + { + "description": "Denies the set_menu command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-menu", + "markdownDescription": "Denies the set_menu command without any pre-configured scope." + }, + { + "description": "Denies the set_show_menu_on_left_click command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-show-menu-on-left-click", + "markdownDescription": "Denies the set_show_menu_on_left_click command without any pre-configured scope." + }, + { + "description": "Denies the set_temp_dir_path command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-temp-dir-path", + "markdownDescription": "Denies the set_temp_dir_path command without any pre-configured scope." + }, + { + "description": "Denies the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-title", + "markdownDescription": "Denies the set_title command without any pre-configured scope." + }, + { + "description": "Denies the set_tooltip command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-tooltip", + "markdownDescription": "Denies the set_tooltip command without any pre-configured scope." + }, + { + "description": "Denies the set_visible command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-visible", + "markdownDescription": "Denies the set_visible command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-webviews`\n- `allow-webview-position`\n- `allow-webview-size`\n- `allow-internal-toggle-devtools`", + "type": "string", + "const": "core:webview:default", + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-webviews`\n- `allow-webview-position`\n- `allow-webview-size`\n- `allow-internal-toggle-devtools`" + }, + { + "description": "Enables the clear_all_browsing_data command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-clear-all-browsing-data", + "markdownDescription": "Enables the clear_all_browsing_data command without any pre-configured scope." + }, + { + "description": "Enables the create_webview command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-create-webview", + "markdownDescription": "Enables the create_webview command without any pre-configured scope." + }, + { + "description": "Enables the create_webview_window command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-create-webview-window", + "markdownDescription": "Enables the create_webview_window command without any pre-configured scope." + }, + { + "description": "Enables the get_all_webviews command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-get-all-webviews", + "markdownDescription": "Enables the get_all_webviews command without any pre-configured scope." + }, + { + "description": "Enables the internal_toggle_devtools command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-internal-toggle-devtools", + "markdownDescription": "Enables the internal_toggle_devtools command without any pre-configured scope." + }, + { + "description": "Enables the print command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-print", + "markdownDescription": "Enables the print command without any pre-configured scope." + }, + { + "description": "Enables the reparent command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-reparent", + "markdownDescription": "Enables the reparent command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_auto_resize command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-auto-resize", + "markdownDescription": "Enables the set_webview_auto_resize command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-background-color", + "markdownDescription": "Enables the set_webview_background_color command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_focus command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-focus", + "markdownDescription": "Enables the set_webview_focus command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-position", + "markdownDescription": "Enables the set_webview_position command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-size", + "markdownDescription": "Enables the set_webview_size command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_zoom command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-zoom", + "markdownDescription": "Enables the set_webview_zoom command without any pre-configured scope." + }, + { + "description": "Enables the webview_close command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-close", + "markdownDescription": "Enables the webview_close command without any pre-configured scope." + }, + { + "description": "Enables the webview_hide command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-hide", + "markdownDescription": "Enables the webview_hide command without any pre-configured scope." + }, + { + "description": "Enables the webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-position", + "markdownDescription": "Enables the webview_position command without any pre-configured scope." + }, + { + "description": "Enables the webview_show command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-show", + "markdownDescription": "Enables the webview_show command without any pre-configured scope." + }, + { + "description": "Enables the webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-size", + "markdownDescription": "Enables the webview_size command without any pre-configured scope." + }, + { + "description": "Denies the clear_all_browsing_data command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-clear-all-browsing-data", + "markdownDescription": "Denies the clear_all_browsing_data command without any pre-configured scope." + }, + { + "description": "Denies the create_webview command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-create-webview", + "markdownDescription": "Denies the create_webview command without any pre-configured scope." + }, + { + "description": "Denies the create_webview_window command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-create-webview-window", + "markdownDescription": "Denies the create_webview_window command without any pre-configured scope." + }, + { + "description": "Denies the get_all_webviews command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-get-all-webviews", + "markdownDescription": "Denies the get_all_webviews command without any pre-configured scope." + }, + { + "description": "Denies the internal_toggle_devtools command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-internal-toggle-devtools", + "markdownDescription": "Denies the internal_toggle_devtools command without any pre-configured scope." + }, + { + "description": "Denies the print command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-print", + "markdownDescription": "Denies the print command without any pre-configured scope." + }, + { + "description": "Denies the reparent command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-reparent", + "markdownDescription": "Denies the reparent command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_auto_resize command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-auto-resize", + "markdownDescription": "Denies the set_webview_auto_resize command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-background-color", + "markdownDescription": "Denies the set_webview_background_color command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_focus command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-focus", + "markdownDescription": "Denies the set_webview_focus command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-position", + "markdownDescription": "Denies the set_webview_position command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-size", + "markdownDescription": "Denies the set_webview_size command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_zoom command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-zoom", + "markdownDescription": "Denies the set_webview_zoom command without any pre-configured scope." + }, + { + "description": "Denies the webview_close command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-close", + "markdownDescription": "Denies the webview_close command without any pre-configured scope." + }, + { + "description": "Denies the webview_hide command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-hide", + "markdownDescription": "Denies the webview_hide command without any pre-configured scope." + }, + { + "description": "Denies the webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-position", + "markdownDescription": "Denies the webview_position command without any pre-configured scope." + }, + { + "description": "Denies the webview_show command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-show", + "markdownDescription": "Denies the webview_show command without any pre-configured scope." + }, + { + "description": "Denies the webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-size", + "markdownDescription": "Denies the webview_size command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-activity-name`\n- `allow-scene-identifier`\n- `allow-internal-toggle-maximize`", + "type": "string", + "const": "core:window:default", + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-activity-name`\n- `allow-scene-identifier`\n- `allow-internal-toggle-maximize`" + }, + { + "description": "Enables the activity_name command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-activity-name", + "markdownDescription": "Enables the activity_name command without any pre-configured scope." + }, + { + "description": "Enables the available_monitors command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-available-monitors", + "markdownDescription": "Enables the available_monitors command without any pre-configured scope." + }, + { + "description": "Enables the center command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-center", + "markdownDescription": "Enables the center command without any pre-configured scope." + }, + { + "description": "Enables the close command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-close", + "markdownDescription": "Enables the close command without any pre-configured scope." + }, + { + "description": "Enables the create command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-create", + "markdownDescription": "Enables the create command without any pre-configured scope." + }, + { + "description": "Enables the current_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-current-monitor", + "markdownDescription": "Enables the current_monitor command without any pre-configured scope." + }, + { + "description": "Enables the cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-cursor-position", + "markdownDescription": "Enables the cursor_position command without any pre-configured scope." + }, + { + "description": "Enables the destroy command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-destroy", + "markdownDescription": "Enables the destroy command without any pre-configured scope." + }, + { + "description": "Enables the get_all_windows command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-get-all-windows", + "markdownDescription": "Enables the get_all_windows command without any pre-configured scope." + }, + { + "description": "Enables the hide command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-hide", + "markdownDescription": "Enables the hide command without any pre-configured scope." + }, + { + "description": "Enables the inner_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-inner-position", + "markdownDescription": "Enables the inner_position command without any pre-configured scope." + }, + { + "description": "Enables the inner_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-inner-size", + "markdownDescription": "Enables the inner_size command without any pre-configured scope." + }, + { + "description": "Enables the internal_toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-internal-toggle-maximize", + "markdownDescription": "Enables the internal_toggle_maximize command without any pre-configured scope." + }, + { + "description": "Enables the is_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-always-on-top", + "markdownDescription": "Enables the is_always_on_top command without any pre-configured scope." + }, + { + "description": "Enables the is_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-closable", + "markdownDescription": "Enables the is_closable command without any pre-configured scope." + }, + { + "description": "Enables the is_decorated command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-decorated", + "markdownDescription": "Enables the is_decorated command without any pre-configured scope." + }, + { + "description": "Enables the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-enabled", + "markdownDescription": "Enables the is_enabled command without any pre-configured scope." + }, + { + "description": "Enables the is_focused command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-focused", + "markdownDescription": "Enables the is_focused command without any pre-configured scope." + }, + { + "description": "Enables the is_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-fullscreen", + "markdownDescription": "Enables the is_fullscreen command without any pre-configured scope." + }, + { + "description": "Enables the is_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-maximizable", + "markdownDescription": "Enables the is_maximizable command without any pre-configured scope." + }, + { + "description": "Enables the is_maximized command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-maximized", + "markdownDescription": "Enables the is_maximized command without any pre-configured scope." + }, + { + "description": "Enables the is_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-minimizable", + "markdownDescription": "Enables the is_minimizable command without any pre-configured scope." + }, + { + "description": "Enables the is_minimized command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-minimized", + "markdownDescription": "Enables the is_minimized command without any pre-configured scope." + }, + { + "description": "Enables the is_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-resizable", + "markdownDescription": "Enables the is_resizable command without any pre-configured scope." + }, + { + "description": "Enables the is_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-visible", + "markdownDescription": "Enables the is_visible command without any pre-configured scope." + }, + { + "description": "Enables the maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-maximize", + "markdownDescription": "Enables the maximize command without any pre-configured scope." + }, + { + "description": "Enables the minimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-minimize", + "markdownDescription": "Enables the minimize command without any pre-configured scope." + }, + { + "description": "Enables the monitor_from_point command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-monitor-from-point", + "markdownDescription": "Enables the monitor_from_point command without any pre-configured scope." + }, + { + "description": "Enables the outer_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-outer-position", + "markdownDescription": "Enables the outer_position command without any pre-configured scope." + }, + { + "description": "Enables the outer_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-outer-size", + "markdownDescription": "Enables the outer_size command without any pre-configured scope." + }, + { + "description": "Enables the primary_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-primary-monitor", + "markdownDescription": "Enables the primary_monitor command without any pre-configured scope." + }, + { + "description": "Enables the request_user_attention command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-request-user-attention", + "markdownDescription": "Enables the request_user_attention command without any pre-configured scope." + }, + { + "description": "Enables the scale_factor command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-scale-factor", + "markdownDescription": "Enables the scale_factor command without any pre-configured scope." + }, + { + "description": "Enables the scene_identifier command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-scene-identifier", + "markdownDescription": "Enables the scene_identifier command without any pre-configured scope." + }, + { + "description": "Enables the set_always_on_bottom command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-always-on-bottom", + "markdownDescription": "Enables the set_always_on_bottom command without any pre-configured scope." + }, + { + "description": "Enables the set_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-always-on-top", + "markdownDescription": "Enables the set_always_on_top command without any pre-configured scope." + }, + { + "description": "Enables the set_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-background-color", + "markdownDescription": "Enables the set_background_color command without any pre-configured scope." + }, + { + "description": "Enables the set_badge_count command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-badge-count", + "markdownDescription": "Enables the set_badge_count command without any pre-configured scope." + }, + { + "description": "Enables the set_badge_label command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-badge-label", + "markdownDescription": "Enables the set_badge_label command without any pre-configured scope." + }, + { + "description": "Enables the set_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-closable", + "markdownDescription": "Enables the set_closable command without any pre-configured scope." + }, + { + "description": "Enables the set_content_protected command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-content-protected", + "markdownDescription": "Enables the set_content_protected command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_grab command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-grab", + "markdownDescription": "Enables the set_cursor_grab command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-icon", + "markdownDescription": "Enables the set_cursor_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-position", + "markdownDescription": "Enables the set_cursor_position command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-visible", + "markdownDescription": "Enables the set_cursor_visible command without any pre-configured scope." + }, + { + "description": "Enables the set_decorations command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-decorations", + "markdownDescription": "Enables the set_decorations command without any pre-configured scope." + }, + { + "description": "Enables the set_effects command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-effects", + "markdownDescription": "Enables the set_effects command without any pre-configured scope." + }, + { + "description": "Enables the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-enabled", + "markdownDescription": "Enables the set_enabled command without any pre-configured scope." + }, + { + "description": "Enables the set_focus command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-focus", + "markdownDescription": "Enables the set_focus command without any pre-configured scope." + }, + { + "description": "Enables the set_focusable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-focusable", + "markdownDescription": "Enables the set_focusable command without any pre-configured scope." + }, + { + "description": "Enables the set_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-fullscreen", + "markdownDescription": "Enables the set_fullscreen command without any pre-configured scope." + }, + { + "description": "Enables the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-icon", + "markdownDescription": "Enables the set_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_ignore_cursor_events command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-ignore-cursor-events", + "markdownDescription": "Enables the set_ignore_cursor_events command without any pre-configured scope." + }, + { + "description": "Enables the set_max_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-max-size", + "markdownDescription": "Enables the set_max_size command without any pre-configured scope." + }, + { + "description": "Enables the set_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-maximizable", + "markdownDescription": "Enables the set_maximizable command without any pre-configured scope." + }, + { + "description": "Enables the set_min_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-min-size", + "markdownDescription": "Enables the set_min_size command without any pre-configured scope." + }, + { + "description": "Enables the set_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-minimizable", + "markdownDescription": "Enables the set_minimizable command without any pre-configured scope." + }, + { + "description": "Enables the set_overlay_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-overlay-icon", + "markdownDescription": "Enables the set_overlay_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-position", + "markdownDescription": "Enables the set_position command without any pre-configured scope." + }, + { + "description": "Enables the set_progress_bar command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-progress-bar", + "markdownDescription": "Enables the set_progress_bar command without any pre-configured scope." + }, + { + "description": "Enables the set_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-resizable", + "markdownDescription": "Enables the set_resizable command without any pre-configured scope." + }, + { + "description": "Enables the set_shadow command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-shadow", + "markdownDescription": "Enables the set_shadow command without any pre-configured scope." + }, + { + "description": "Enables the set_simple_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-simple-fullscreen", + "markdownDescription": "Enables the set_simple_fullscreen command without any pre-configured scope." + }, + { + "description": "Enables the set_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-size", + "markdownDescription": "Enables the set_size command without any pre-configured scope." + }, + { + "description": "Enables the set_size_constraints command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-size-constraints", + "markdownDescription": "Enables the set_size_constraints command without any pre-configured scope." + }, + { + "description": "Enables the set_skip_taskbar command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-skip-taskbar", + "markdownDescription": "Enables the set_skip_taskbar command without any pre-configured scope." + }, + { + "description": "Enables the set_theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-theme", + "markdownDescription": "Enables the set_theme command without any pre-configured scope." + }, + { + "description": "Enables the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-title", + "markdownDescription": "Enables the set_title command without any pre-configured scope." + }, + { + "description": "Enables the set_title_bar_style command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-title-bar-style", + "markdownDescription": "Enables the set_title_bar_style command without any pre-configured scope." + }, + { + "description": "Enables the set_visible_on_all_workspaces command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-visible-on-all-workspaces", + "markdownDescription": "Enables the set_visible_on_all_workspaces command without any pre-configured scope." + }, + { + "description": "Enables the show command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-show", + "markdownDescription": "Enables the show command without any pre-configured scope." + }, + { + "description": "Enables the start_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-start-dragging", + "markdownDescription": "Enables the start_dragging command without any pre-configured scope." + }, + { + "description": "Enables the start_resize_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-start-resize-dragging", + "markdownDescription": "Enables the start_resize_dragging command without any pre-configured scope." + }, + { + "description": "Enables the theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-theme", + "markdownDescription": "Enables the theme command without any pre-configured scope." + }, + { + "description": "Enables the title command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-title", + "markdownDescription": "Enables the title command without any pre-configured scope." + }, + { + "description": "Enables the toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-toggle-maximize", + "markdownDescription": "Enables the toggle_maximize command without any pre-configured scope." + }, + { + "description": "Enables the unmaximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-unmaximize", + "markdownDescription": "Enables the unmaximize command without any pre-configured scope." + }, + { + "description": "Enables the unminimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-unminimize", + "markdownDescription": "Enables the unminimize command without any pre-configured scope." + }, + { + "description": "Denies the activity_name command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-activity-name", + "markdownDescription": "Denies the activity_name command without any pre-configured scope." + }, + { + "description": "Denies the available_monitors command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-available-monitors", + "markdownDescription": "Denies the available_monitors command without any pre-configured scope." + }, + { + "description": "Denies the center command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-center", + "markdownDescription": "Denies the center command without any pre-configured scope." + }, + { + "description": "Denies the close command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-close", + "markdownDescription": "Denies the close command without any pre-configured scope." + }, + { + "description": "Denies the create command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-create", + "markdownDescription": "Denies the create command without any pre-configured scope." + }, + { + "description": "Denies the current_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-current-monitor", + "markdownDescription": "Denies the current_monitor command without any pre-configured scope." + }, + { + "description": "Denies the cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-cursor-position", + "markdownDescription": "Denies the cursor_position command without any pre-configured scope." + }, + { + "description": "Denies the destroy command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-destroy", + "markdownDescription": "Denies the destroy command without any pre-configured scope." + }, + { + "description": "Denies the get_all_windows command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-get-all-windows", + "markdownDescription": "Denies the get_all_windows command without any pre-configured scope." + }, + { + "description": "Denies the hide command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-hide", + "markdownDescription": "Denies the hide command without any pre-configured scope." + }, + { + "description": "Denies the inner_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-inner-position", + "markdownDescription": "Denies the inner_position command without any pre-configured scope." + }, + { + "description": "Denies the inner_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-inner-size", + "markdownDescription": "Denies the inner_size command without any pre-configured scope." + }, + { + "description": "Denies the internal_toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-internal-toggle-maximize", + "markdownDescription": "Denies the internal_toggle_maximize command without any pre-configured scope." + }, + { + "description": "Denies the is_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-always-on-top", + "markdownDescription": "Denies the is_always_on_top command without any pre-configured scope." + }, + { + "description": "Denies the is_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-closable", + "markdownDescription": "Denies the is_closable command without any pre-configured scope." + }, + { + "description": "Denies the is_decorated command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-decorated", + "markdownDescription": "Denies the is_decorated command without any pre-configured scope." + }, + { + "description": "Denies the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-enabled", + "markdownDescription": "Denies the is_enabled command without any pre-configured scope." + }, + { + "description": "Denies the is_focused command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-focused", + "markdownDescription": "Denies the is_focused command without any pre-configured scope." + }, + { + "description": "Denies the is_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-fullscreen", + "markdownDescription": "Denies the is_fullscreen command without any pre-configured scope." + }, + { + "description": "Denies the is_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-maximizable", + "markdownDescription": "Denies the is_maximizable command without any pre-configured scope." + }, + { + "description": "Denies the is_maximized command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-maximized", + "markdownDescription": "Denies the is_maximized command without any pre-configured scope." + }, + { + "description": "Denies the is_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-minimizable", + "markdownDescription": "Denies the is_minimizable command without any pre-configured scope." + }, + { + "description": "Denies the is_minimized command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-minimized", + "markdownDescription": "Denies the is_minimized command without any pre-configured scope." + }, + { + "description": "Denies the is_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-resizable", + "markdownDescription": "Denies the is_resizable command without any pre-configured scope." + }, + { + "description": "Denies the is_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-visible", + "markdownDescription": "Denies the is_visible command without any pre-configured scope." + }, + { + "description": "Denies the maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-maximize", + "markdownDescription": "Denies the maximize command without any pre-configured scope." + }, + { + "description": "Denies the minimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-minimize", + "markdownDescription": "Denies the minimize command without any pre-configured scope." + }, + { + "description": "Denies the monitor_from_point command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-monitor-from-point", + "markdownDescription": "Denies the monitor_from_point command without any pre-configured scope." + }, + { + "description": "Denies the outer_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-outer-position", + "markdownDescription": "Denies the outer_position command without any pre-configured scope." + }, + { + "description": "Denies the outer_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-outer-size", + "markdownDescription": "Denies the outer_size command without any pre-configured scope." + }, + { + "description": "Denies the primary_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-primary-monitor", + "markdownDescription": "Denies the primary_monitor command without any pre-configured scope." + }, + { + "description": "Denies the request_user_attention command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-request-user-attention", + "markdownDescription": "Denies the request_user_attention command without any pre-configured scope." + }, + { + "description": "Denies the scale_factor command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-scale-factor", + "markdownDescription": "Denies the scale_factor command without any pre-configured scope." + }, + { + "description": "Denies the scene_identifier command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-scene-identifier", + "markdownDescription": "Denies the scene_identifier command without any pre-configured scope." + }, + { + "description": "Denies the set_always_on_bottom command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-always-on-bottom", + "markdownDescription": "Denies the set_always_on_bottom command without any pre-configured scope." + }, + { + "description": "Denies the set_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-always-on-top", + "markdownDescription": "Denies the set_always_on_top command without any pre-configured scope." + }, + { + "description": "Denies the set_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-background-color", + "markdownDescription": "Denies the set_background_color command without any pre-configured scope." + }, + { + "description": "Denies the set_badge_count command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-badge-count", + "markdownDescription": "Denies the set_badge_count command without any pre-configured scope." + }, + { + "description": "Denies the set_badge_label command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-badge-label", + "markdownDescription": "Denies the set_badge_label command without any pre-configured scope." + }, + { + "description": "Denies the set_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-closable", + "markdownDescription": "Denies the set_closable command without any pre-configured scope." + }, + { + "description": "Denies the set_content_protected command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-content-protected", + "markdownDescription": "Denies the set_content_protected command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_grab command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-grab", + "markdownDescription": "Denies the set_cursor_grab command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-icon", + "markdownDescription": "Denies the set_cursor_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-position", + "markdownDescription": "Denies the set_cursor_position command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-visible", + "markdownDescription": "Denies the set_cursor_visible command without any pre-configured scope." + }, + { + "description": "Denies the set_decorations command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-decorations", + "markdownDescription": "Denies the set_decorations command without any pre-configured scope." + }, + { + "description": "Denies the set_effects command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-effects", + "markdownDescription": "Denies the set_effects command without any pre-configured scope." + }, + { + "description": "Denies the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-enabled", + "markdownDescription": "Denies the set_enabled command without any pre-configured scope." + }, + { + "description": "Denies the set_focus command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-focus", + "markdownDescription": "Denies the set_focus command without any pre-configured scope." + }, + { + "description": "Denies the set_focusable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-focusable", + "markdownDescription": "Denies the set_focusable command without any pre-configured scope." + }, + { + "description": "Denies the set_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-fullscreen", + "markdownDescription": "Denies the set_fullscreen command without any pre-configured scope." + }, + { + "description": "Denies the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-icon", + "markdownDescription": "Denies the set_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_ignore_cursor_events command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-ignore-cursor-events", + "markdownDescription": "Denies the set_ignore_cursor_events command without any pre-configured scope." + }, + { + "description": "Denies the set_max_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-max-size", + "markdownDescription": "Denies the set_max_size command without any pre-configured scope." + }, + { + "description": "Denies the set_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-maximizable", + "markdownDescription": "Denies the set_maximizable command without any pre-configured scope." + }, + { + "description": "Denies the set_min_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-min-size", + "markdownDescription": "Denies the set_min_size command without any pre-configured scope." + }, + { + "description": "Denies the set_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-minimizable", + "markdownDescription": "Denies the set_minimizable command without any pre-configured scope." + }, + { + "description": "Denies the set_overlay_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-overlay-icon", + "markdownDescription": "Denies the set_overlay_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-position", + "markdownDescription": "Denies the set_position command without any pre-configured scope." + }, + { + "description": "Denies the set_progress_bar command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-progress-bar", + "markdownDescription": "Denies the set_progress_bar command without any pre-configured scope." + }, + { + "description": "Denies the set_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-resizable", + "markdownDescription": "Denies the set_resizable command without any pre-configured scope." + }, + { + "description": "Denies the set_shadow command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-shadow", + "markdownDescription": "Denies the set_shadow command without any pre-configured scope." + }, + { + "description": "Denies the set_simple_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-simple-fullscreen", + "markdownDescription": "Denies the set_simple_fullscreen command without any pre-configured scope." + }, + { + "description": "Denies the set_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-size", + "markdownDescription": "Denies the set_size command without any pre-configured scope." + }, + { + "description": "Denies the set_size_constraints command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-size-constraints", + "markdownDescription": "Denies the set_size_constraints command without any pre-configured scope." + }, + { + "description": "Denies the set_skip_taskbar command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-skip-taskbar", + "markdownDescription": "Denies the set_skip_taskbar command without any pre-configured scope." + }, + { + "description": "Denies the set_theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-theme", + "markdownDescription": "Denies the set_theme command without any pre-configured scope." + }, + { + "description": "Denies the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-title", + "markdownDescription": "Denies the set_title command without any pre-configured scope." + }, + { + "description": "Denies the set_title_bar_style command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-title-bar-style", + "markdownDescription": "Denies the set_title_bar_style command without any pre-configured scope." + }, + { + "description": "Denies the set_visible_on_all_workspaces command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-visible-on-all-workspaces", + "markdownDescription": "Denies the set_visible_on_all_workspaces command without any pre-configured scope." + }, + { + "description": "Denies the show command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-show", + "markdownDescription": "Denies the show command without any pre-configured scope." + }, + { + "description": "Denies the start_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-start-dragging", + "markdownDescription": "Denies the start_dragging command without any pre-configured scope." + }, + { + "description": "Denies the start_resize_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-start-resize-dragging", + "markdownDescription": "Denies the start_resize_dragging command without any pre-configured scope." + }, + { + "description": "Denies the theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-theme", + "markdownDescription": "Denies the theme command without any pre-configured scope." + }, + { + "description": "Denies the title command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-title", + "markdownDescription": "Denies the title command without any pre-configured scope." + }, + { + "description": "Denies the toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-toggle-maximize", + "markdownDescription": "Denies the toggle_maximize command without any pre-configured scope." + }, + { + "description": "Denies the unmaximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-unmaximize", + "markdownDescription": "Denies the unmaximize command without any pre-configured scope." + }, + { + "description": "Denies the unminimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-unminimize", + "markdownDescription": "Denies the unminimize command without any pre-configured scope." + }, + { + "description": "No features are enabled by default, as we believe\nthe shortcuts can be inherently dangerous and it is\napplication specific if specific shortcuts should be\nregistered or unregistered.\n", + "type": "string", + "const": "global-shortcut:default", + "markdownDescription": "No features are enabled by default, as we believe\nthe shortcuts can be inherently dangerous and it is\napplication specific if specific shortcuts should be\nregistered or unregistered.\n" + }, + { + "description": "Enables the is_registered command without any pre-configured scope.", + "type": "string", + "const": "global-shortcut:allow-is-registered", + "markdownDescription": "Enables the is_registered command without any pre-configured scope." + }, + { + "description": "Enables the register command without any pre-configured scope.", + "type": "string", + "const": "global-shortcut:allow-register", + "markdownDescription": "Enables the register command without any pre-configured scope." + }, + { + "description": "Enables the register_all command without any pre-configured scope.", + "type": "string", + "const": "global-shortcut:allow-register-all", + "markdownDescription": "Enables the register_all command without any pre-configured scope." + }, + { + "description": "Enables the unregister command without any pre-configured scope.", + "type": "string", + "const": "global-shortcut:allow-unregister", + "markdownDescription": "Enables the unregister command without any pre-configured scope." + }, + { + "description": "Enables the unregister_all command without any pre-configured scope.", + "type": "string", + "const": "global-shortcut:allow-unregister-all", + "markdownDescription": "Enables the unregister_all command without any pre-configured scope." + }, + { + "description": "Denies the is_registered command without any pre-configured scope.", + "type": "string", + "const": "global-shortcut:deny-is-registered", + "markdownDescription": "Denies the is_registered command without any pre-configured scope." + }, + { + "description": "Denies the register command without any pre-configured scope.", + "type": "string", + "const": "global-shortcut:deny-register", + "markdownDescription": "Denies the register command without any pre-configured scope." + }, + { + "description": "Denies the register_all command without any pre-configured scope.", + "type": "string", + "const": "global-shortcut:deny-register-all", + "markdownDescription": "Denies the register_all command without any pre-configured scope." + }, + { + "description": "Denies the unregister command without any pre-configured scope.", + "type": "string", + "const": "global-shortcut:deny-unregister", + "markdownDescription": "Denies the unregister command without any pre-configured scope." + }, + { + "description": "Denies the unregister_all command without any pre-configured scope.", + "type": "string", + "const": "global-shortcut:deny-unregister-all", + "markdownDescription": "Denies the unregister_all command without any pre-configured scope." + } + ] + }, + "Value": { + "description": "All supported ACL values.", + "anyOf": [ + { + "description": "Represents a null JSON value.", + "type": "null" + }, + { + "description": "Represents a [`bool`].", + "type": "boolean" + }, + { + "description": "Represents a valid ACL [`Number`].", + "allOf": [ + { + "$ref": "#/definitions/Number" + } + ] + }, + { + "description": "Represents a [`String`].", + "type": "string" + }, + { + "description": "Represents a list of other [`Value`]s.", + "type": "array", + "items": { + "$ref": "#/definitions/Value" + } + }, + { + "description": "Represents a map of [`String`] keys to [`Value`]s.", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Value" + } + } + ] + }, + "Number": { + "description": "A valid ACL number.", + "anyOf": [ + { + "description": "Represents an [`i64`].", + "type": "integer", + "format": "int64" + }, + { + "description": "Represents a [`f64`].", + "type": "number", + "format": "double" + } + ] + }, + "Target": { + "description": "Platform target.", + "oneOf": [ + { + "description": "MacOS.", + "type": "string", + "enum": [ + "macOS" + ] + }, + { + "description": "Windows.", + "type": "string", + "enum": [ + "windows" + ] + }, + { + "description": "Linux.", + "type": "string", + "enum": [ + "linux" + ] + }, + { + "description": "Android.", + "type": "string", + "enum": [ + "android" + ] + }, + { + "description": "iOS.", + "type": "string", + "enum": [ + "iOS" + ] + } + ] + } + } +} \ No newline at end of file diff --git a/desktop/src-tauri/gen/schemas/macOS-schema.json b/desktop/src-tauri/gen/schemas/macOS-schema.json new file mode 100644 index 0000000..3680773 --- /dev/null +++ b/desktop/src-tauri/gen/schemas/macOS-schema.json @@ -0,0 +1,2358 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "CapabilityFile", + "description": "Capability formats accepted in a capability file.", + "anyOf": [ + { + "description": "A single capability.", + "allOf": [ + { + "$ref": "#/definitions/Capability" + } + ] + }, + { + "description": "A list of capabilities.", + "type": "array", + "items": { + "$ref": "#/definitions/Capability" + } + }, + { + "description": "A list of capabilities.", + "type": "object", + "required": [ + "capabilities" + ], + "properties": { + "capabilities": { + "description": "The list of capabilities.", + "type": "array", + "items": { + "$ref": "#/definitions/Capability" + } + } + } + } + ], + "definitions": { + "Capability": { + "description": "A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\n\nIt controls application windows' and webviews' fine grained access to the Tauri core, application, or plugin commands. If a webview or its window is not matching any capability then it has no access to the IPC layer at all.\n\nThis can be done to create groups of windows, based on their required system access, which can reduce impact of frontend vulnerabilities in less privileged windows. Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`. A Window can have none, one, or multiple associated capabilities.\n\n## Example\n\n```json { \"identifier\": \"main-user-files-write\", \"description\": \"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.\", \"windows\": [ \"main\" ], \"permissions\": [ \"core:default\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] }, ], \"platforms\": [\"macOS\",\"windows\"] } ```", + "type": "object", + "required": [ + "identifier", + "permissions" + ], + "properties": { + "identifier": { + "description": "Identifier of the capability.\n\n## Example\n\n`main-user-files-write`", + "type": "string" + }, + "description": { + "description": "Description of what the capability is intended to allow on associated windows.\n\nIt should contain a description of what the grouped permissions should allow.\n\n## Example\n\nThis capability allows the `main` window access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.", + "default": "", + "type": "string" + }, + "remote": { + "description": "Configure remote URLs that can use the capability permissions.\n\nThis setting is optional and defaults to not being set, as our default use case is that the content is served from our local application.\n\n:::caution Make sure you understand the security implications of providing remote sources with local system access. :::\n\n## Example\n\n```json { \"urls\": [\"https://*.mydomain.dev\"] } ```", + "anyOf": [ + { + "$ref": "#/definitions/CapabilityRemote" + }, + { + "type": "null" + } + ] + }, + "local": { + "description": "Whether this capability is enabled for local app URLs or not. Defaults to `true`.", + "default": true, + "type": "boolean" + }, + "windows": { + "description": "List of windows that are affected by this capability. Can be a glob pattern.\n\nIf a window label matches any of the patterns in this list, the capability will be enabled on all the webviews of that window, regardless of the value of [`Self::webviews`].\n\nOn multiwebview windows, prefer specifying [`Self::webviews`] and omitting [`Self::windows`] for a fine grained access control.\n\n## Example\n\n`[\"main\"]`", + "type": "array", + "items": { + "type": "string" + } + }, + "webviews": { + "description": "List of webviews that are affected by this capability. Can be a glob pattern.\n\nThe capability will be enabled on all the webviews whose label matches any of the patterns in this list, regardless of whether the webview's window label matches a pattern in [`Self::windows`].\n\n## Example\n\n`[\"sub-webview-one\", \"sub-webview-two\"]`", + "type": "array", + "items": { + "type": "string" + } + }, + "permissions": { + "description": "List of permissions attached to this capability.\n\nMust include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`. For commands directly implemented in the application itself only `${permission-name}` is required.\n\n## Example\n\n```json [ \"core:default\", \"shell:allow-open\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] } ] ```", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionEntry" + }, + "uniqueItems": true + }, + "platforms": { + "description": "Limit which target platforms this capability applies to.\n\nBy default all platforms are targeted.\n\n## Example\n\n`[\"macOS\",\"windows\"]`", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Target" + } + } + } + }, + "CapabilityRemote": { + "description": "Configuration for remote URLs that are associated with the capability.", + "type": "object", + "required": [ + "urls" + ], + "properties": { + "urls": { + "description": "Remote domains this capability refers to using the [URLPattern standard](https://urlpattern.spec.whatwg.org/).\n\n## Examples\n\n- \"https://*.mydomain.dev\": allows subdomains of mydomain.dev - \"https://mydomain.dev/api/*\": allows any subpath of mydomain.dev/api", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "PermissionEntry": { + "description": "An entry for a permission value in a [`Capability`] can be either a raw permission [`Identifier`] or an object that references a permission and extends its scope.", + "anyOf": [ + { + "description": "Reference a permission or permission set by identifier.", + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ] + }, + { + "description": "Reference a permission or permission set by identifier and extends its scope.", + "type": "object", + "allOf": [ + { + "properties": { + "identifier": { + "description": "Identifier of the permission or permission set.", + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ] + }, + "allow": { + "description": "Data that defines what is allowed by the scope.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + }, + "deny": { + "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + } + } + } + ], + "required": [ + "identifier" + ] + } + ] + }, + "Identifier": { + "description": "Permission identifier", + "oneOf": [ + { + "description": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`", + "type": "string", + "const": "core:default", + "markdownDescription": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`" + }, + { + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-supports-multiple-windows`", + "type": "string", + "const": "core:app:default", + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-supports-multiple-windows`" + }, + { + "description": "Enables the app_hide command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-app-hide", + "markdownDescription": "Enables the app_hide command without any pre-configured scope." + }, + { + "description": "Enables the app_show command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-app-show", + "markdownDescription": "Enables the app_show command without any pre-configured scope." + }, + { + "description": "Enables the bundle_type command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-bundle-type", + "markdownDescription": "Enables the bundle_type command without any pre-configured scope." + }, + { + "description": "Enables the default_window_icon command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-default-window-icon", + "markdownDescription": "Enables the default_window_icon command without any pre-configured scope." + }, + { + "description": "Enables the fetch_data_store_identifiers command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-fetch-data-store-identifiers", + "markdownDescription": "Enables the fetch_data_store_identifiers command without any pre-configured scope." + }, + { + "description": "Enables the identifier command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-identifier", + "markdownDescription": "Enables the identifier command without any pre-configured scope." + }, + { + "description": "Enables the name command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-name", + "markdownDescription": "Enables the name command without any pre-configured scope." + }, + { + "description": "Enables the register_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-register-listener", + "markdownDescription": "Enables the register_listener command without any pre-configured scope." + }, + { + "description": "Enables the remove_data_store command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-remove-data-store", + "markdownDescription": "Enables the remove_data_store command without any pre-configured scope." + }, + { + "description": "Enables the remove_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-remove-listener", + "markdownDescription": "Enables the remove_listener command without any pre-configured scope." + }, + { + "description": "Enables the set_app_theme command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-set-app-theme", + "markdownDescription": "Enables the set_app_theme command without any pre-configured scope." + }, + { + "description": "Enables the set_dock_visibility command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-set-dock-visibility", + "markdownDescription": "Enables the set_dock_visibility command without any pre-configured scope." + }, + { + "description": "Enables the supports_multiple_windows command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-supports-multiple-windows", + "markdownDescription": "Enables the supports_multiple_windows command without any pre-configured scope." + }, + { + "description": "Enables the tauri_version command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-tauri-version", + "markdownDescription": "Enables the tauri_version command without any pre-configured scope." + }, + { + "description": "Enables the version command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-version", + "markdownDescription": "Enables the version command without any pre-configured scope." + }, + { + "description": "Denies the app_hide command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-app-hide", + "markdownDescription": "Denies the app_hide command without any pre-configured scope." + }, + { + "description": "Denies the app_show command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-app-show", + "markdownDescription": "Denies the app_show command without any pre-configured scope." + }, + { + "description": "Denies the bundle_type command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-bundle-type", + "markdownDescription": "Denies the bundle_type command without any pre-configured scope." + }, + { + "description": "Denies the default_window_icon command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-default-window-icon", + "markdownDescription": "Denies the default_window_icon command without any pre-configured scope." + }, + { + "description": "Denies the fetch_data_store_identifiers command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-fetch-data-store-identifiers", + "markdownDescription": "Denies the fetch_data_store_identifiers command without any pre-configured scope." + }, + { + "description": "Denies the identifier command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-identifier", + "markdownDescription": "Denies the identifier command without any pre-configured scope." + }, + { + "description": "Denies the name command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-name", + "markdownDescription": "Denies the name command without any pre-configured scope." + }, + { + "description": "Denies the register_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-register-listener", + "markdownDescription": "Denies the register_listener command without any pre-configured scope." + }, + { + "description": "Denies the remove_data_store command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-remove-data-store", + "markdownDescription": "Denies the remove_data_store command without any pre-configured scope." + }, + { + "description": "Denies the remove_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-remove-listener", + "markdownDescription": "Denies the remove_listener command without any pre-configured scope." + }, + { + "description": "Denies the set_app_theme command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-set-app-theme", + "markdownDescription": "Denies the set_app_theme command without any pre-configured scope." + }, + { + "description": "Denies the set_dock_visibility command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-set-dock-visibility", + "markdownDescription": "Denies the set_dock_visibility command without any pre-configured scope." + }, + { + "description": "Denies the supports_multiple_windows command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-supports-multiple-windows", + "markdownDescription": "Denies the supports_multiple_windows command without any pre-configured scope." + }, + { + "description": "Denies the tauri_version command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-tauri-version", + "markdownDescription": "Denies the tauri_version command without any pre-configured scope." + }, + { + "description": "Denies the version command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-version", + "markdownDescription": "Denies the version command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-listen`\n- `allow-unlisten`\n- `allow-emit`\n- `allow-emit-to`", + "type": "string", + "const": "core:event:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-listen`\n- `allow-unlisten`\n- `allow-emit`\n- `allow-emit-to`" + }, + { + "description": "Enables the emit command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-emit", + "markdownDescription": "Enables the emit command without any pre-configured scope." + }, + { + "description": "Enables the emit_to command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-emit-to", + "markdownDescription": "Enables the emit_to command without any pre-configured scope." + }, + { + "description": "Enables the listen command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-listen", + "markdownDescription": "Enables the listen command without any pre-configured scope." + }, + { + "description": "Enables the unlisten command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-unlisten", + "markdownDescription": "Enables the unlisten command without any pre-configured scope." + }, + { + "description": "Denies the emit command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-emit", + "markdownDescription": "Denies the emit command without any pre-configured scope." + }, + { + "description": "Denies the emit_to command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-emit-to", + "markdownDescription": "Denies the emit_to command without any pre-configured scope." + }, + { + "description": "Denies the listen command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-listen", + "markdownDescription": "Denies the listen command without any pre-configured scope." + }, + { + "description": "Denies the unlisten command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-unlisten", + "markdownDescription": "Denies the unlisten command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-from-bytes`\n- `allow-from-path`\n- `allow-rgba`\n- `allow-size`", + "type": "string", + "const": "core:image:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-from-bytes`\n- `allow-from-path`\n- `allow-rgba`\n- `allow-size`" + }, + { + "description": "Enables the from_bytes command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-from-bytes", + "markdownDescription": "Enables the from_bytes command without any pre-configured scope." + }, + { + "description": "Enables the from_path command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-from-path", + "markdownDescription": "Enables the from_path command without any pre-configured scope." + }, + { + "description": "Enables the new command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-new", + "markdownDescription": "Enables the new command without any pre-configured scope." + }, + { + "description": "Enables the rgba command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-rgba", + "markdownDescription": "Enables the rgba command without any pre-configured scope." + }, + { + "description": "Enables the size command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-size", + "markdownDescription": "Enables the size command without any pre-configured scope." + }, + { + "description": "Denies the from_bytes command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-from-bytes", + "markdownDescription": "Denies the from_bytes command without any pre-configured scope." + }, + { + "description": "Denies the from_path command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-from-path", + "markdownDescription": "Denies the from_path command without any pre-configured scope." + }, + { + "description": "Denies the new command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-new", + "markdownDescription": "Denies the new command without any pre-configured scope." + }, + { + "description": "Denies the rgba command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-rgba", + "markdownDescription": "Denies the rgba command without any pre-configured scope." + }, + { + "description": "Denies the size command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-size", + "markdownDescription": "Denies the size command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-append`\n- `allow-prepend`\n- `allow-insert`\n- `allow-remove`\n- `allow-remove-at`\n- `allow-items`\n- `allow-get`\n- `allow-popup`\n- `allow-create-default`\n- `allow-set-as-app-menu`\n- `allow-set-as-window-menu`\n- `allow-text`\n- `allow-set-text`\n- `allow-is-enabled`\n- `allow-set-enabled`\n- `allow-set-accelerator`\n- `allow-set-as-windows-menu-for-nsapp`\n- `allow-set-as-help-menu-for-nsapp`\n- `allow-is-checked`\n- `allow-set-checked`\n- `allow-set-icon`", + "type": "string", + "const": "core:menu:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-append`\n- `allow-prepend`\n- `allow-insert`\n- `allow-remove`\n- `allow-remove-at`\n- `allow-items`\n- `allow-get`\n- `allow-popup`\n- `allow-create-default`\n- `allow-set-as-app-menu`\n- `allow-set-as-window-menu`\n- `allow-text`\n- `allow-set-text`\n- `allow-is-enabled`\n- `allow-set-enabled`\n- `allow-set-accelerator`\n- `allow-set-as-windows-menu-for-nsapp`\n- `allow-set-as-help-menu-for-nsapp`\n- `allow-is-checked`\n- `allow-set-checked`\n- `allow-set-icon`" + }, + { + "description": "Enables the append command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-append", + "markdownDescription": "Enables the append command without any pre-configured scope." + }, + { + "description": "Enables the create_default command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-create-default", + "markdownDescription": "Enables the create_default command without any pre-configured scope." + }, + { + "description": "Enables the get command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-get", + "markdownDescription": "Enables the get command without any pre-configured scope." + }, + { + "description": "Enables the insert command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-insert", + "markdownDescription": "Enables the insert command without any pre-configured scope." + }, + { + "description": "Enables the is_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-is-checked", + "markdownDescription": "Enables the is_checked command without any pre-configured scope." + }, + { + "description": "Enables the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-is-enabled", + "markdownDescription": "Enables the is_enabled command without any pre-configured scope." + }, + { + "description": "Enables the items command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-items", + "markdownDescription": "Enables the items command without any pre-configured scope." + }, + { + "description": "Enables the new command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-new", + "markdownDescription": "Enables the new command without any pre-configured scope." + }, + { + "description": "Enables the popup command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-popup", + "markdownDescription": "Enables the popup command without any pre-configured scope." + }, + { + "description": "Enables the prepend command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-prepend", + "markdownDescription": "Enables the prepend command without any pre-configured scope." + }, + { + "description": "Enables the remove command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-remove", + "markdownDescription": "Enables the remove command without any pre-configured scope." + }, + { + "description": "Enables the remove_at command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-remove-at", + "markdownDescription": "Enables the remove_at command without any pre-configured scope." + }, + { + "description": "Enables the set_accelerator command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-accelerator", + "markdownDescription": "Enables the set_accelerator command without any pre-configured scope." + }, + { + "description": "Enables the set_as_app_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-app-menu", + "markdownDescription": "Enables the set_as_app_menu command without any pre-configured scope." + }, + { + "description": "Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-help-menu-for-nsapp", + "markdownDescription": "Enables the set_as_help_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Enables the set_as_window_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-window-menu", + "markdownDescription": "Enables the set_as_window_menu command without any pre-configured scope." + }, + { + "description": "Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-windows-menu-for-nsapp", + "markdownDescription": "Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Enables the set_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-checked", + "markdownDescription": "Enables the set_checked command without any pre-configured scope." + }, + { + "description": "Enables the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-enabled", + "markdownDescription": "Enables the set_enabled command without any pre-configured scope." + }, + { + "description": "Enables the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-icon", + "markdownDescription": "Enables the set_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-text", + "markdownDescription": "Enables the set_text command without any pre-configured scope." + }, + { + "description": "Enables the text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-text", + "markdownDescription": "Enables the text command without any pre-configured scope." + }, + { + "description": "Denies the append command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-append", + "markdownDescription": "Denies the append command without any pre-configured scope." + }, + { + "description": "Denies the create_default command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-create-default", + "markdownDescription": "Denies the create_default command without any pre-configured scope." + }, + { + "description": "Denies the get command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-get", + "markdownDescription": "Denies the get command without any pre-configured scope." + }, + { + "description": "Denies the insert command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-insert", + "markdownDescription": "Denies the insert command without any pre-configured scope." + }, + { + "description": "Denies the is_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-is-checked", + "markdownDescription": "Denies the is_checked command without any pre-configured scope." + }, + { + "description": "Denies the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-is-enabled", + "markdownDescription": "Denies the is_enabled command without any pre-configured scope." + }, + { + "description": "Denies the items command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-items", + "markdownDescription": "Denies the items command without any pre-configured scope." + }, + { + "description": "Denies the new command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-new", + "markdownDescription": "Denies the new command without any pre-configured scope." + }, + { + "description": "Denies the popup command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-popup", + "markdownDescription": "Denies the popup command without any pre-configured scope." + }, + { + "description": "Denies the prepend command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-prepend", + "markdownDescription": "Denies the prepend command without any pre-configured scope." + }, + { + "description": "Denies the remove command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-remove", + "markdownDescription": "Denies the remove command without any pre-configured scope." + }, + { + "description": "Denies the remove_at command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-remove-at", + "markdownDescription": "Denies the remove_at command without any pre-configured scope." + }, + { + "description": "Denies the set_accelerator command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-accelerator", + "markdownDescription": "Denies the set_accelerator command without any pre-configured scope." + }, + { + "description": "Denies the set_as_app_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-app-menu", + "markdownDescription": "Denies the set_as_app_menu command without any pre-configured scope." + }, + { + "description": "Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-help-menu-for-nsapp", + "markdownDescription": "Denies the set_as_help_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Denies the set_as_window_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-window-menu", + "markdownDescription": "Denies the set_as_window_menu command without any pre-configured scope." + }, + { + "description": "Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-windows-menu-for-nsapp", + "markdownDescription": "Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Denies the set_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-checked", + "markdownDescription": "Denies the set_checked command without any pre-configured scope." + }, + { + "description": "Denies the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-enabled", + "markdownDescription": "Denies the set_enabled command without any pre-configured scope." + }, + { + "description": "Denies the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-icon", + "markdownDescription": "Denies the set_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-text", + "markdownDescription": "Denies the set_text command without any pre-configured scope." + }, + { + "description": "Denies the text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-text", + "markdownDescription": "Denies the text command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-resolve-directory`\n- `allow-resolve`\n- `allow-normalize`\n- `allow-join`\n- `allow-dirname`\n- `allow-extname`\n- `allow-basename`\n- `allow-is-absolute`", + "type": "string", + "const": "core:path:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-resolve-directory`\n- `allow-resolve`\n- `allow-normalize`\n- `allow-join`\n- `allow-dirname`\n- `allow-extname`\n- `allow-basename`\n- `allow-is-absolute`" + }, + { + "description": "Enables the basename command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-basename", + "markdownDescription": "Enables the basename command without any pre-configured scope." + }, + { + "description": "Enables the dirname command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-dirname", + "markdownDescription": "Enables the dirname command without any pre-configured scope." + }, + { + "description": "Enables the extname command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-extname", + "markdownDescription": "Enables the extname command without any pre-configured scope." + }, + { + "description": "Enables the is_absolute command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-is-absolute", + "markdownDescription": "Enables the is_absolute command without any pre-configured scope." + }, + { + "description": "Enables the join command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-join", + "markdownDescription": "Enables the join command without any pre-configured scope." + }, + { + "description": "Enables the normalize command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-normalize", + "markdownDescription": "Enables the normalize command without any pre-configured scope." + }, + { + "description": "Enables the resolve command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-resolve", + "markdownDescription": "Enables the resolve command without any pre-configured scope." + }, + { + "description": "Enables the resolve_directory command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-resolve-directory", + "markdownDescription": "Enables the resolve_directory command without any pre-configured scope." + }, + { + "description": "Denies the basename command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-basename", + "markdownDescription": "Denies the basename command without any pre-configured scope." + }, + { + "description": "Denies the dirname command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-dirname", + "markdownDescription": "Denies the dirname command without any pre-configured scope." + }, + { + "description": "Denies the extname command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-extname", + "markdownDescription": "Denies the extname command without any pre-configured scope." + }, + { + "description": "Denies the is_absolute command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-is-absolute", + "markdownDescription": "Denies the is_absolute command without any pre-configured scope." + }, + { + "description": "Denies the join command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-join", + "markdownDescription": "Denies the join command without any pre-configured scope." + }, + { + "description": "Denies the normalize command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-normalize", + "markdownDescription": "Denies the normalize command without any pre-configured scope." + }, + { + "description": "Denies the resolve command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-resolve", + "markdownDescription": "Denies the resolve command without any pre-configured scope." + }, + { + "description": "Denies the resolve_directory command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-resolve-directory", + "markdownDescription": "Denies the resolve_directory command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-close`", + "type": "string", + "const": "core:resources:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-close`" + }, + { + "description": "Enables the close command without any pre-configured scope.", + "type": "string", + "const": "core:resources:allow-close", + "markdownDescription": "Enables the close command without any pre-configured scope." + }, + { + "description": "Denies the close command without any pre-configured scope.", + "type": "string", + "const": "core:resources:deny-close", + "markdownDescription": "Denies the close command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-icon-with-as-template`\n- `allow-set-show-menu-on-left-click`", + "type": "string", + "const": "core:tray:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-icon-with-as-template`\n- `allow-set-show-menu-on-left-click`" + }, + { + "description": "Enables the get_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-get-by-id", + "markdownDescription": "Enables the get_by_id command without any pre-configured scope." + }, + { + "description": "Enables the new command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-new", + "markdownDescription": "Enables the new command without any pre-configured scope." + }, + { + "description": "Enables the remove_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-remove-by-id", + "markdownDescription": "Enables the remove_by_id command without any pre-configured scope." + }, + { + "description": "Enables the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-icon", + "markdownDescription": "Enables the set_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_icon_as_template command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-icon-as-template", + "markdownDescription": "Enables the set_icon_as_template command without any pre-configured scope." + }, + { + "description": "Enables the set_icon_with_as_template command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-icon-with-as-template", + "markdownDescription": "Enables the set_icon_with_as_template command without any pre-configured scope." + }, + { + "description": "Enables the set_menu command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-menu", + "markdownDescription": "Enables the set_menu command without any pre-configured scope." + }, + { + "description": "Enables the set_show_menu_on_left_click command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-show-menu-on-left-click", + "markdownDescription": "Enables the set_show_menu_on_left_click command without any pre-configured scope." + }, + { + "description": "Enables the set_temp_dir_path command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-temp-dir-path", + "markdownDescription": "Enables the set_temp_dir_path command without any pre-configured scope." + }, + { + "description": "Enables the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-title", + "markdownDescription": "Enables the set_title command without any pre-configured scope." + }, + { + "description": "Enables the set_tooltip command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-tooltip", + "markdownDescription": "Enables the set_tooltip command without any pre-configured scope." + }, + { + "description": "Enables the set_visible command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-visible", + "markdownDescription": "Enables the set_visible command without any pre-configured scope." + }, + { + "description": "Denies the get_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-get-by-id", + "markdownDescription": "Denies the get_by_id command without any pre-configured scope." + }, + { + "description": "Denies the new command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-new", + "markdownDescription": "Denies the new command without any pre-configured scope." + }, + { + "description": "Denies the remove_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-remove-by-id", + "markdownDescription": "Denies the remove_by_id command without any pre-configured scope." + }, + { + "description": "Denies the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-icon", + "markdownDescription": "Denies the set_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_icon_as_template command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-icon-as-template", + "markdownDescription": "Denies the set_icon_as_template command without any pre-configured scope." + }, + { + "description": "Denies the set_icon_with_as_template command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-icon-with-as-template", + "markdownDescription": "Denies the set_icon_with_as_template command without any pre-configured scope." + }, + { + "description": "Denies the set_menu command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-menu", + "markdownDescription": "Denies the set_menu command without any pre-configured scope." + }, + { + "description": "Denies the set_show_menu_on_left_click command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-show-menu-on-left-click", + "markdownDescription": "Denies the set_show_menu_on_left_click command without any pre-configured scope." + }, + { + "description": "Denies the set_temp_dir_path command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-temp-dir-path", + "markdownDescription": "Denies the set_temp_dir_path command without any pre-configured scope." + }, + { + "description": "Denies the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-title", + "markdownDescription": "Denies the set_title command without any pre-configured scope." + }, + { + "description": "Denies the set_tooltip command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-tooltip", + "markdownDescription": "Denies the set_tooltip command without any pre-configured scope." + }, + { + "description": "Denies the set_visible command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-visible", + "markdownDescription": "Denies the set_visible command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-webviews`\n- `allow-webview-position`\n- `allow-webview-size`\n- `allow-internal-toggle-devtools`", + "type": "string", + "const": "core:webview:default", + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-webviews`\n- `allow-webview-position`\n- `allow-webview-size`\n- `allow-internal-toggle-devtools`" + }, + { + "description": "Enables the clear_all_browsing_data command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-clear-all-browsing-data", + "markdownDescription": "Enables the clear_all_browsing_data command without any pre-configured scope." + }, + { + "description": "Enables the create_webview command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-create-webview", + "markdownDescription": "Enables the create_webview command without any pre-configured scope." + }, + { + "description": "Enables the create_webview_window command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-create-webview-window", + "markdownDescription": "Enables the create_webview_window command without any pre-configured scope." + }, + { + "description": "Enables the get_all_webviews command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-get-all-webviews", + "markdownDescription": "Enables the get_all_webviews command without any pre-configured scope." + }, + { + "description": "Enables the internal_toggle_devtools command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-internal-toggle-devtools", + "markdownDescription": "Enables the internal_toggle_devtools command without any pre-configured scope." + }, + { + "description": "Enables the print command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-print", + "markdownDescription": "Enables the print command without any pre-configured scope." + }, + { + "description": "Enables the reparent command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-reparent", + "markdownDescription": "Enables the reparent command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_auto_resize command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-auto-resize", + "markdownDescription": "Enables the set_webview_auto_resize command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-background-color", + "markdownDescription": "Enables the set_webview_background_color command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_focus command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-focus", + "markdownDescription": "Enables the set_webview_focus command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-position", + "markdownDescription": "Enables the set_webview_position command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-size", + "markdownDescription": "Enables the set_webview_size command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_zoom command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-zoom", + "markdownDescription": "Enables the set_webview_zoom command without any pre-configured scope." + }, + { + "description": "Enables the webview_close command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-close", + "markdownDescription": "Enables the webview_close command without any pre-configured scope." + }, + { + "description": "Enables the webview_hide command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-hide", + "markdownDescription": "Enables the webview_hide command without any pre-configured scope." + }, + { + "description": "Enables the webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-position", + "markdownDescription": "Enables the webview_position command without any pre-configured scope." + }, + { + "description": "Enables the webview_show command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-show", + "markdownDescription": "Enables the webview_show command without any pre-configured scope." + }, + { + "description": "Enables the webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-size", + "markdownDescription": "Enables the webview_size command without any pre-configured scope." + }, + { + "description": "Denies the clear_all_browsing_data command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-clear-all-browsing-data", + "markdownDescription": "Denies the clear_all_browsing_data command without any pre-configured scope." + }, + { + "description": "Denies the create_webview command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-create-webview", + "markdownDescription": "Denies the create_webview command without any pre-configured scope." + }, + { + "description": "Denies the create_webview_window command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-create-webview-window", + "markdownDescription": "Denies the create_webview_window command without any pre-configured scope." + }, + { + "description": "Denies the get_all_webviews command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-get-all-webviews", + "markdownDescription": "Denies the get_all_webviews command without any pre-configured scope." + }, + { + "description": "Denies the internal_toggle_devtools command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-internal-toggle-devtools", + "markdownDescription": "Denies the internal_toggle_devtools command without any pre-configured scope." + }, + { + "description": "Denies the print command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-print", + "markdownDescription": "Denies the print command without any pre-configured scope." + }, + { + "description": "Denies the reparent command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-reparent", + "markdownDescription": "Denies the reparent command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_auto_resize command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-auto-resize", + "markdownDescription": "Denies the set_webview_auto_resize command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-background-color", + "markdownDescription": "Denies the set_webview_background_color command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_focus command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-focus", + "markdownDescription": "Denies the set_webview_focus command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-position", + "markdownDescription": "Denies the set_webview_position command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-size", + "markdownDescription": "Denies the set_webview_size command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_zoom command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-zoom", + "markdownDescription": "Denies the set_webview_zoom command without any pre-configured scope." + }, + { + "description": "Denies the webview_close command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-close", + "markdownDescription": "Denies the webview_close command without any pre-configured scope." + }, + { + "description": "Denies the webview_hide command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-hide", + "markdownDescription": "Denies the webview_hide command without any pre-configured scope." + }, + { + "description": "Denies the webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-position", + "markdownDescription": "Denies the webview_position command without any pre-configured scope." + }, + { + "description": "Denies the webview_show command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-show", + "markdownDescription": "Denies the webview_show command without any pre-configured scope." + }, + { + "description": "Denies the webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-size", + "markdownDescription": "Denies the webview_size command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-activity-name`\n- `allow-scene-identifier`\n- `allow-internal-toggle-maximize`", + "type": "string", + "const": "core:window:default", + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-activity-name`\n- `allow-scene-identifier`\n- `allow-internal-toggle-maximize`" + }, + { + "description": "Enables the activity_name command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-activity-name", + "markdownDescription": "Enables the activity_name command without any pre-configured scope." + }, + { + "description": "Enables the available_monitors command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-available-monitors", + "markdownDescription": "Enables the available_monitors command without any pre-configured scope." + }, + { + "description": "Enables the center command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-center", + "markdownDescription": "Enables the center command without any pre-configured scope." + }, + { + "description": "Enables the close command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-close", + "markdownDescription": "Enables the close command without any pre-configured scope." + }, + { + "description": "Enables the create command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-create", + "markdownDescription": "Enables the create command without any pre-configured scope." + }, + { + "description": "Enables the current_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-current-monitor", + "markdownDescription": "Enables the current_monitor command without any pre-configured scope." + }, + { + "description": "Enables the cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-cursor-position", + "markdownDescription": "Enables the cursor_position command without any pre-configured scope." + }, + { + "description": "Enables the destroy command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-destroy", + "markdownDescription": "Enables the destroy command without any pre-configured scope." + }, + { + "description": "Enables the get_all_windows command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-get-all-windows", + "markdownDescription": "Enables the get_all_windows command without any pre-configured scope." + }, + { + "description": "Enables the hide command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-hide", + "markdownDescription": "Enables the hide command without any pre-configured scope." + }, + { + "description": "Enables the inner_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-inner-position", + "markdownDescription": "Enables the inner_position command without any pre-configured scope." + }, + { + "description": "Enables the inner_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-inner-size", + "markdownDescription": "Enables the inner_size command without any pre-configured scope." + }, + { + "description": "Enables the internal_toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-internal-toggle-maximize", + "markdownDescription": "Enables the internal_toggle_maximize command without any pre-configured scope." + }, + { + "description": "Enables the is_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-always-on-top", + "markdownDescription": "Enables the is_always_on_top command without any pre-configured scope." + }, + { + "description": "Enables the is_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-closable", + "markdownDescription": "Enables the is_closable command without any pre-configured scope." + }, + { + "description": "Enables the is_decorated command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-decorated", + "markdownDescription": "Enables the is_decorated command without any pre-configured scope." + }, + { + "description": "Enables the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-enabled", + "markdownDescription": "Enables the is_enabled command without any pre-configured scope." + }, + { + "description": "Enables the is_focused command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-focused", + "markdownDescription": "Enables the is_focused command without any pre-configured scope." + }, + { + "description": "Enables the is_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-fullscreen", + "markdownDescription": "Enables the is_fullscreen command without any pre-configured scope." + }, + { + "description": "Enables the is_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-maximizable", + "markdownDescription": "Enables the is_maximizable command without any pre-configured scope." + }, + { + "description": "Enables the is_maximized command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-maximized", + "markdownDescription": "Enables the is_maximized command without any pre-configured scope." + }, + { + "description": "Enables the is_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-minimizable", + "markdownDescription": "Enables the is_minimizable command without any pre-configured scope." + }, + { + "description": "Enables the is_minimized command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-minimized", + "markdownDescription": "Enables the is_minimized command without any pre-configured scope." + }, + { + "description": "Enables the is_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-resizable", + "markdownDescription": "Enables the is_resizable command without any pre-configured scope." + }, + { + "description": "Enables the is_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-visible", + "markdownDescription": "Enables the is_visible command without any pre-configured scope." + }, + { + "description": "Enables the maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-maximize", + "markdownDescription": "Enables the maximize command without any pre-configured scope." + }, + { + "description": "Enables the minimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-minimize", + "markdownDescription": "Enables the minimize command without any pre-configured scope." + }, + { + "description": "Enables the monitor_from_point command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-monitor-from-point", + "markdownDescription": "Enables the monitor_from_point command without any pre-configured scope." + }, + { + "description": "Enables the outer_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-outer-position", + "markdownDescription": "Enables the outer_position command without any pre-configured scope." + }, + { + "description": "Enables the outer_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-outer-size", + "markdownDescription": "Enables the outer_size command without any pre-configured scope." + }, + { + "description": "Enables the primary_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-primary-monitor", + "markdownDescription": "Enables the primary_monitor command without any pre-configured scope." + }, + { + "description": "Enables the request_user_attention command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-request-user-attention", + "markdownDescription": "Enables the request_user_attention command without any pre-configured scope." + }, + { + "description": "Enables the scale_factor command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-scale-factor", + "markdownDescription": "Enables the scale_factor command without any pre-configured scope." + }, + { + "description": "Enables the scene_identifier command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-scene-identifier", + "markdownDescription": "Enables the scene_identifier command without any pre-configured scope." + }, + { + "description": "Enables the set_always_on_bottom command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-always-on-bottom", + "markdownDescription": "Enables the set_always_on_bottom command without any pre-configured scope." + }, + { + "description": "Enables the set_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-always-on-top", + "markdownDescription": "Enables the set_always_on_top command without any pre-configured scope." + }, + { + "description": "Enables the set_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-background-color", + "markdownDescription": "Enables the set_background_color command without any pre-configured scope." + }, + { + "description": "Enables the set_badge_count command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-badge-count", + "markdownDescription": "Enables the set_badge_count command without any pre-configured scope." + }, + { + "description": "Enables the set_badge_label command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-badge-label", + "markdownDescription": "Enables the set_badge_label command without any pre-configured scope." + }, + { + "description": "Enables the set_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-closable", + "markdownDescription": "Enables the set_closable command without any pre-configured scope." + }, + { + "description": "Enables the set_content_protected command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-content-protected", + "markdownDescription": "Enables the set_content_protected command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_grab command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-grab", + "markdownDescription": "Enables the set_cursor_grab command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-icon", + "markdownDescription": "Enables the set_cursor_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-position", + "markdownDescription": "Enables the set_cursor_position command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-visible", + "markdownDescription": "Enables the set_cursor_visible command without any pre-configured scope." + }, + { + "description": "Enables the set_decorations command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-decorations", + "markdownDescription": "Enables the set_decorations command without any pre-configured scope." + }, + { + "description": "Enables the set_effects command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-effects", + "markdownDescription": "Enables the set_effects command without any pre-configured scope." + }, + { + "description": "Enables the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-enabled", + "markdownDescription": "Enables the set_enabled command without any pre-configured scope." + }, + { + "description": "Enables the set_focus command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-focus", + "markdownDescription": "Enables the set_focus command without any pre-configured scope." + }, + { + "description": "Enables the set_focusable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-focusable", + "markdownDescription": "Enables the set_focusable command without any pre-configured scope." + }, + { + "description": "Enables the set_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-fullscreen", + "markdownDescription": "Enables the set_fullscreen command without any pre-configured scope." + }, + { + "description": "Enables the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-icon", + "markdownDescription": "Enables the set_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_ignore_cursor_events command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-ignore-cursor-events", + "markdownDescription": "Enables the set_ignore_cursor_events command without any pre-configured scope." + }, + { + "description": "Enables the set_max_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-max-size", + "markdownDescription": "Enables the set_max_size command without any pre-configured scope." + }, + { + "description": "Enables the set_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-maximizable", + "markdownDescription": "Enables the set_maximizable command without any pre-configured scope." + }, + { + "description": "Enables the set_min_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-min-size", + "markdownDescription": "Enables the set_min_size command without any pre-configured scope." + }, + { + "description": "Enables the set_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-minimizable", + "markdownDescription": "Enables the set_minimizable command without any pre-configured scope." + }, + { + "description": "Enables the set_overlay_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-overlay-icon", + "markdownDescription": "Enables the set_overlay_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-position", + "markdownDescription": "Enables the set_position command without any pre-configured scope." + }, + { + "description": "Enables the set_progress_bar command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-progress-bar", + "markdownDescription": "Enables the set_progress_bar command without any pre-configured scope." + }, + { + "description": "Enables the set_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-resizable", + "markdownDescription": "Enables the set_resizable command without any pre-configured scope." + }, + { + "description": "Enables the set_shadow command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-shadow", + "markdownDescription": "Enables the set_shadow command without any pre-configured scope." + }, + { + "description": "Enables the set_simple_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-simple-fullscreen", + "markdownDescription": "Enables the set_simple_fullscreen command without any pre-configured scope." + }, + { + "description": "Enables the set_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-size", + "markdownDescription": "Enables the set_size command without any pre-configured scope." + }, + { + "description": "Enables the set_size_constraints command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-size-constraints", + "markdownDescription": "Enables the set_size_constraints command without any pre-configured scope." + }, + { + "description": "Enables the set_skip_taskbar command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-skip-taskbar", + "markdownDescription": "Enables the set_skip_taskbar command without any pre-configured scope." + }, + { + "description": "Enables the set_theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-theme", + "markdownDescription": "Enables the set_theme command without any pre-configured scope." + }, + { + "description": "Enables the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-title", + "markdownDescription": "Enables the set_title command without any pre-configured scope." + }, + { + "description": "Enables the set_title_bar_style command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-title-bar-style", + "markdownDescription": "Enables the set_title_bar_style command without any pre-configured scope." + }, + { + "description": "Enables the set_visible_on_all_workspaces command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-visible-on-all-workspaces", + "markdownDescription": "Enables the set_visible_on_all_workspaces command without any pre-configured scope." + }, + { + "description": "Enables the show command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-show", + "markdownDescription": "Enables the show command without any pre-configured scope." + }, + { + "description": "Enables the start_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-start-dragging", + "markdownDescription": "Enables the start_dragging command without any pre-configured scope." + }, + { + "description": "Enables the start_resize_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-start-resize-dragging", + "markdownDescription": "Enables the start_resize_dragging command without any pre-configured scope." + }, + { + "description": "Enables the theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-theme", + "markdownDescription": "Enables the theme command without any pre-configured scope." + }, + { + "description": "Enables the title command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-title", + "markdownDescription": "Enables the title command without any pre-configured scope." + }, + { + "description": "Enables the toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-toggle-maximize", + "markdownDescription": "Enables the toggle_maximize command without any pre-configured scope." + }, + { + "description": "Enables the unmaximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-unmaximize", + "markdownDescription": "Enables the unmaximize command without any pre-configured scope." + }, + { + "description": "Enables the unminimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-unminimize", + "markdownDescription": "Enables the unminimize command without any pre-configured scope." + }, + { + "description": "Denies the activity_name command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-activity-name", + "markdownDescription": "Denies the activity_name command without any pre-configured scope." + }, + { + "description": "Denies the available_monitors command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-available-monitors", + "markdownDescription": "Denies the available_monitors command without any pre-configured scope." + }, + { + "description": "Denies the center command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-center", + "markdownDescription": "Denies the center command without any pre-configured scope." + }, + { + "description": "Denies the close command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-close", + "markdownDescription": "Denies the close command without any pre-configured scope." + }, + { + "description": "Denies the create command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-create", + "markdownDescription": "Denies the create command without any pre-configured scope." + }, + { + "description": "Denies the current_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-current-monitor", + "markdownDescription": "Denies the current_monitor command without any pre-configured scope." + }, + { + "description": "Denies the cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-cursor-position", + "markdownDescription": "Denies the cursor_position command without any pre-configured scope." + }, + { + "description": "Denies the destroy command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-destroy", + "markdownDescription": "Denies the destroy command without any pre-configured scope." + }, + { + "description": "Denies the get_all_windows command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-get-all-windows", + "markdownDescription": "Denies the get_all_windows command without any pre-configured scope." + }, + { + "description": "Denies the hide command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-hide", + "markdownDescription": "Denies the hide command without any pre-configured scope." + }, + { + "description": "Denies the inner_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-inner-position", + "markdownDescription": "Denies the inner_position command without any pre-configured scope." + }, + { + "description": "Denies the inner_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-inner-size", + "markdownDescription": "Denies the inner_size command without any pre-configured scope." + }, + { + "description": "Denies the internal_toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-internal-toggle-maximize", + "markdownDescription": "Denies the internal_toggle_maximize command without any pre-configured scope." + }, + { + "description": "Denies the is_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-always-on-top", + "markdownDescription": "Denies the is_always_on_top command without any pre-configured scope." + }, + { + "description": "Denies the is_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-closable", + "markdownDescription": "Denies the is_closable command without any pre-configured scope." + }, + { + "description": "Denies the is_decorated command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-decorated", + "markdownDescription": "Denies the is_decorated command without any pre-configured scope." + }, + { + "description": "Denies the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-enabled", + "markdownDescription": "Denies the is_enabled command without any pre-configured scope." + }, + { + "description": "Denies the is_focused command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-focused", + "markdownDescription": "Denies the is_focused command without any pre-configured scope." + }, + { + "description": "Denies the is_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-fullscreen", + "markdownDescription": "Denies the is_fullscreen command without any pre-configured scope." + }, + { + "description": "Denies the is_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-maximizable", + "markdownDescription": "Denies the is_maximizable command without any pre-configured scope." + }, + { + "description": "Denies the is_maximized command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-maximized", + "markdownDescription": "Denies the is_maximized command without any pre-configured scope." + }, + { + "description": "Denies the is_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-minimizable", + "markdownDescription": "Denies the is_minimizable command without any pre-configured scope." + }, + { + "description": "Denies the is_minimized command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-minimized", + "markdownDescription": "Denies the is_minimized command without any pre-configured scope." + }, + { + "description": "Denies the is_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-resizable", + "markdownDescription": "Denies the is_resizable command without any pre-configured scope." + }, + { + "description": "Denies the is_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-visible", + "markdownDescription": "Denies the is_visible command without any pre-configured scope." + }, + { + "description": "Denies the maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-maximize", + "markdownDescription": "Denies the maximize command without any pre-configured scope." + }, + { + "description": "Denies the minimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-minimize", + "markdownDescription": "Denies the minimize command without any pre-configured scope." + }, + { + "description": "Denies the monitor_from_point command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-monitor-from-point", + "markdownDescription": "Denies the monitor_from_point command without any pre-configured scope." + }, + { + "description": "Denies the outer_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-outer-position", + "markdownDescription": "Denies the outer_position command without any pre-configured scope." + }, + { + "description": "Denies the outer_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-outer-size", + "markdownDescription": "Denies the outer_size command without any pre-configured scope." + }, + { + "description": "Denies the primary_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-primary-monitor", + "markdownDescription": "Denies the primary_monitor command without any pre-configured scope." + }, + { + "description": "Denies the request_user_attention command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-request-user-attention", + "markdownDescription": "Denies the request_user_attention command without any pre-configured scope." + }, + { + "description": "Denies the scale_factor command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-scale-factor", + "markdownDescription": "Denies the scale_factor command without any pre-configured scope." + }, + { + "description": "Denies the scene_identifier command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-scene-identifier", + "markdownDescription": "Denies the scene_identifier command without any pre-configured scope." + }, + { + "description": "Denies the set_always_on_bottom command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-always-on-bottom", + "markdownDescription": "Denies the set_always_on_bottom command without any pre-configured scope." + }, + { + "description": "Denies the set_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-always-on-top", + "markdownDescription": "Denies the set_always_on_top command without any pre-configured scope." + }, + { + "description": "Denies the set_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-background-color", + "markdownDescription": "Denies the set_background_color command without any pre-configured scope." + }, + { + "description": "Denies the set_badge_count command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-badge-count", + "markdownDescription": "Denies the set_badge_count command without any pre-configured scope." + }, + { + "description": "Denies the set_badge_label command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-badge-label", + "markdownDescription": "Denies the set_badge_label command without any pre-configured scope." + }, + { + "description": "Denies the set_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-closable", + "markdownDescription": "Denies the set_closable command without any pre-configured scope." + }, + { + "description": "Denies the set_content_protected command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-content-protected", + "markdownDescription": "Denies the set_content_protected command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_grab command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-grab", + "markdownDescription": "Denies the set_cursor_grab command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-icon", + "markdownDescription": "Denies the set_cursor_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-position", + "markdownDescription": "Denies the set_cursor_position command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-visible", + "markdownDescription": "Denies the set_cursor_visible command without any pre-configured scope." + }, + { + "description": "Denies the set_decorations command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-decorations", + "markdownDescription": "Denies the set_decorations command without any pre-configured scope." + }, + { + "description": "Denies the set_effects command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-effects", + "markdownDescription": "Denies the set_effects command without any pre-configured scope." + }, + { + "description": "Denies the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-enabled", + "markdownDescription": "Denies the set_enabled command without any pre-configured scope." + }, + { + "description": "Denies the set_focus command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-focus", + "markdownDescription": "Denies the set_focus command without any pre-configured scope." + }, + { + "description": "Denies the set_focusable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-focusable", + "markdownDescription": "Denies the set_focusable command without any pre-configured scope." + }, + { + "description": "Denies the set_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-fullscreen", + "markdownDescription": "Denies the set_fullscreen command without any pre-configured scope." + }, + { + "description": "Denies the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-icon", + "markdownDescription": "Denies the set_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_ignore_cursor_events command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-ignore-cursor-events", + "markdownDescription": "Denies the set_ignore_cursor_events command without any pre-configured scope." + }, + { + "description": "Denies the set_max_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-max-size", + "markdownDescription": "Denies the set_max_size command without any pre-configured scope." + }, + { + "description": "Denies the set_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-maximizable", + "markdownDescription": "Denies the set_maximizable command without any pre-configured scope." + }, + { + "description": "Denies the set_min_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-min-size", + "markdownDescription": "Denies the set_min_size command without any pre-configured scope." + }, + { + "description": "Denies the set_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-minimizable", + "markdownDescription": "Denies the set_minimizable command without any pre-configured scope." + }, + { + "description": "Denies the set_overlay_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-overlay-icon", + "markdownDescription": "Denies the set_overlay_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-position", + "markdownDescription": "Denies the set_position command without any pre-configured scope." + }, + { + "description": "Denies the set_progress_bar command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-progress-bar", + "markdownDescription": "Denies the set_progress_bar command without any pre-configured scope." + }, + { + "description": "Denies the set_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-resizable", + "markdownDescription": "Denies the set_resizable command without any pre-configured scope." + }, + { + "description": "Denies the set_shadow command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-shadow", + "markdownDescription": "Denies the set_shadow command without any pre-configured scope." + }, + { + "description": "Denies the set_simple_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-simple-fullscreen", + "markdownDescription": "Denies the set_simple_fullscreen command without any pre-configured scope." + }, + { + "description": "Denies the set_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-size", + "markdownDescription": "Denies the set_size command without any pre-configured scope." + }, + { + "description": "Denies the set_size_constraints command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-size-constraints", + "markdownDescription": "Denies the set_size_constraints command without any pre-configured scope." + }, + { + "description": "Denies the set_skip_taskbar command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-skip-taskbar", + "markdownDescription": "Denies the set_skip_taskbar command without any pre-configured scope." + }, + { + "description": "Denies the set_theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-theme", + "markdownDescription": "Denies the set_theme command without any pre-configured scope." + }, + { + "description": "Denies the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-title", + "markdownDescription": "Denies the set_title command without any pre-configured scope." + }, + { + "description": "Denies the set_title_bar_style command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-title-bar-style", + "markdownDescription": "Denies the set_title_bar_style command without any pre-configured scope." + }, + { + "description": "Denies the set_visible_on_all_workspaces command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-visible-on-all-workspaces", + "markdownDescription": "Denies the set_visible_on_all_workspaces command without any pre-configured scope." + }, + { + "description": "Denies the show command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-show", + "markdownDescription": "Denies the show command without any pre-configured scope." + }, + { + "description": "Denies the start_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-start-dragging", + "markdownDescription": "Denies the start_dragging command without any pre-configured scope." + }, + { + "description": "Denies the start_resize_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-start-resize-dragging", + "markdownDescription": "Denies the start_resize_dragging command without any pre-configured scope." + }, + { + "description": "Denies the theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-theme", + "markdownDescription": "Denies the theme command without any pre-configured scope." + }, + { + "description": "Denies the title command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-title", + "markdownDescription": "Denies the title command without any pre-configured scope." + }, + { + "description": "Denies the toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-toggle-maximize", + "markdownDescription": "Denies the toggle_maximize command without any pre-configured scope." + }, + { + "description": "Denies the unmaximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-unmaximize", + "markdownDescription": "Denies the unmaximize command without any pre-configured scope." + }, + { + "description": "Denies the unminimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-unminimize", + "markdownDescription": "Denies the unminimize command without any pre-configured scope." + }, + { + "description": "No features are enabled by default, as we believe\nthe shortcuts can be inherently dangerous and it is\napplication specific if specific shortcuts should be\nregistered or unregistered.\n", + "type": "string", + "const": "global-shortcut:default", + "markdownDescription": "No features are enabled by default, as we believe\nthe shortcuts can be inherently dangerous and it is\napplication specific if specific shortcuts should be\nregistered or unregistered.\n" + }, + { + "description": "Enables the is_registered command without any pre-configured scope.", + "type": "string", + "const": "global-shortcut:allow-is-registered", + "markdownDescription": "Enables the is_registered command without any pre-configured scope." + }, + { + "description": "Enables the register command without any pre-configured scope.", + "type": "string", + "const": "global-shortcut:allow-register", + "markdownDescription": "Enables the register command without any pre-configured scope." + }, + { + "description": "Enables the register_all command without any pre-configured scope.", + "type": "string", + "const": "global-shortcut:allow-register-all", + "markdownDescription": "Enables the register_all command without any pre-configured scope." + }, + { + "description": "Enables the unregister command without any pre-configured scope.", + "type": "string", + "const": "global-shortcut:allow-unregister", + "markdownDescription": "Enables the unregister command without any pre-configured scope." + }, + { + "description": "Enables the unregister_all command without any pre-configured scope.", + "type": "string", + "const": "global-shortcut:allow-unregister-all", + "markdownDescription": "Enables the unregister_all command without any pre-configured scope." + }, + { + "description": "Denies the is_registered command without any pre-configured scope.", + "type": "string", + "const": "global-shortcut:deny-is-registered", + "markdownDescription": "Denies the is_registered command without any pre-configured scope." + }, + { + "description": "Denies the register command without any pre-configured scope.", + "type": "string", + "const": "global-shortcut:deny-register", + "markdownDescription": "Denies the register command without any pre-configured scope." + }, + { + "description": "Denies the register_all command without any pre-configured scope.", + "type": "string", + "const": "global-shortcut:deny-register-all", + "markdownDescription": "Denies the register_all command without any pre-configured scope." + }, + { + "description": "Denies the unregister command without any pre-configured scope.", + "type": "string", + "const": "global-shortcut:deny-unregister", + "markdownDescription": "Denies the unregister command without any pre-configured scope." + }, + { + "description": "Denies the unregister_all command without any pre-configured scope.", + "type": "string", + "const": "global-shortcut:deny-unregister-all", + "markdownDescription": "Denies the unregister_all command without any pre-configured scope." + } + ] + }, + "Value": { + "description": "All supported ACL values.", + "anyOf": [ + { + "description": "Represents a null JSON value.", + "type": "null" + }, + { + "description": "Represents a [`bool`].", + "type": "boolean" + }, + { + "description": "Represents a valid ACL [`Number`].", + "allOf": [ + { + "$ref": "#/definitions/Number" + } + ] + }, + { + "description": "Represents a [`String`].", + "type": "string" + }, + { + "description": "Represents a list of other [`Value`]s.", + "type": "array", + "items": { + "$ref": "#/definitions/Value" + } + }, + { + "description": "Represents a map of [`String`] keys to [`Value`]s.", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Value" + } + } + ] + }, + "Number": { + "description": "A valid ACL number.", + "anyOf": [ + { + "description": "Represents an [`i64`].", + "type": "integer", + "format": "int64" + }, + { + "description": "Represents a [`f64`].", + "type": "number", + "format": "double" + } + ] + }, + "Target": { + "description": "Platform target.", + "oneOf": [ + { + "description": "MacOS.", + "type": "string", + "enum": [ + "macOS" + ] + }, + { + "description": "Windows.", + "type": "string", + "enum": [ + "windows" + ] + }, + { + "description": "Linux.", + "type": "string", + "enum": [ + "linux" + ] + }, + { + "description": "Android.", + "type": "string", + "enum": [ + "android" + ] + }, + { + "description": "iOS.", + "type": "string", + "enum": [ + "iOS" + ] + } + ] + } + } +} \ No newline at end of file diff --git a/desktop/src-tauri/icons/icon.png b/desktop/src-tauri/icons/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..13a84d42dcaa69f6267c8760c5af2dece3d56cfa GIT binary patch literal 2185 zcmbW1Pe>F|9LK-!&E!t2wzCDbMmXc25=zQ6%52jyw?AIYqC<3PQAp?zTM-&W`!W$C z!!F%6ii8M)phI*iT4~WkPzYgB2RHW8p>P9(Aw~MVaouSp%JH;c-tY7K_nr6lMrX(2 zQvXIjfYSCOk>da-$)Od|&rIL@Ux3!x9%((%_v!oO=q=u=tD`^lqp^E6TCpCu{jmO& z-*;yC@r&(yESKfYA%%a4E&njsg z35ZZ^&_G3cs2N?!va1*n4Rg>~kIi&WfdTQwhl=PN)7jgsVycNRK1*xN1ca7LYe8kQ zERIMn%>ix2|=wj(A5FBonvDb=Ki*S>a*B!ZA$~H&VOQ|?=yTj5z2yb;*F6cW=D#>nc)$wpO zQaoAZNE5VDbU(u7P72F$BTRCk#o&4wY+DRgIYC_pEh>AFrD^&QD=&fym~_H`gn{x^ z=ntk_9fnKaASkw`1j7uYT2@~~b7fq_t>T94EeO+fTj=<2G}ox0%HPf`n&!WiOoC_~ z?~RvLT~PIcIVnrPqc!es8itp#(%+g&BbE;Ge{*t`yqlA=)ScXfC!0-#)fIdb*_JLo zS-jG1S9BGew)qF((PodhYpI3+TlOa`@Skp==IL>|O{tDW<`z7wSSMb*Yx8Wnp;#)c z*xafmuP-^uOb)YgzTnD!Q-ubRwu|#VpDD<{zZR81e`d$RfOqK69{Q%Zyb!>nf@Etm zB6L5-PZPQ;F9of&)$ROX1(F7MAO|ov=4Z)E+Neoxv{-k6^?dqA8p({&9GC6|fw9vn z#E%UO(Yb>_C|&^!E;({3j;QTWLCWbp0KA?iN5-aZ07I|VafGhcaMaCrXvE#sk<9LM z!2v2fwVI(sf`e0gDJs|-a`ds{A`dpc2vl9#ieUm|n) VS@qdR$3BqrXg}Byd2%4y{~PE2N6Y{K literal 0 HcmV?d00001 diff --git a/desktop/src-tauri/src/api.rs b/desktop/src-tauri/src/api.rs new file mode 100644 index 0000000..4865666 --- /dev/null +++ b/desktop/src-tauri/src/api.rs @@ -0,0 +1,292 @@ +//! 后端 HTTP API 封装 + 窗口控制命令。 + +use serde_json::{json, Value}; +use std::sync::atomic::{AtomicBool, Ordering}; +use tauri::{AppHandle, Manager}; + +/// 全局暂停开关(11B 托盘菜单"暂停使用"):暂停时快捷键不再触发听写。 +#[derive(Default)] +pub struct Paused(AtomicBool); + +pub fn is_app_paused(app: &AppHandle) -> bool { + app.state::().0.load(Ordering::Relaxed) +} + +#[tauri::command] +pub fn is_paused(app: AppHandle) -> bool { + is_app_paused(&app) +} + +/// 切换暂停状态,返回新状态。 +#[tauri::command] +pub fn toggle_pause(app: AppHandle) -> bool { + let paused = &app.state::().0; + let next = !paused.load(Ordering::Relaxed); + paused.store(next, Ordering::Relaxed); + if next { + crate::dictation::stop(&app, true); // 录音中暂停 → 直接取消 + } + if let Some(tray) = app.tray_by_id("main") { + let _ = tray.set_tooltip(Some(if next { "dudu — 已暂停" } else { "dudu — 就绪" })); + } + next +} + +fn base(app: &AppHandle) -> (String, String) { + let s = app.state::().get(); + (s.server_url, s.token) +} + +async fn get_json(app: &AppHandle, path: &str, authed: bool) -> Option { + let (url, token) = base(app); + let mut req = reqwest::Client::new().get(format!("{url}{path}")); + if authed { + if token.is_empty() { + return None; + } + req = req.bearer_auth(token); + } + let resp = req.send().await.ok()?; + if !resp.status().is_success() { + return None; + } + resp.json().await.ok() +} + +#[tauri::command] +pub async fn fetch_me(app: AppHandle) -> Option { + get_json(&app, "/v1/me", true).await +} + +#[tauri::command] +pub async fn fetch_packs(app: AppHandle) -> Option { + let v = get_json(&app, "/v1/packs", false).await?; + v.get("packs").cloned() +} + +#[tauri::command] +pub async fn login_qr_create(app: AppHandle) -> Option { + let (url, _) = base(&app); + reqwest::Client::new() + .post(format!("{url}/v1/auth/qr")) + .send() + .await + .ok()? + .json() + .await + .ok() +} + +#[tauri::command] +pub async fn login_qr_poll(app: AppHandle, state: String) -> Option { + let v = get_json(&app, &format!("/v1/auth/qr/{state}"), false).await?; + if v.get("status").and_then(|s| s.as_str()) == Some("confirmed") { + if let Some(token) = v.get("token").and_then(|t| t.as_str()) { + let store = app.state::(); + let mut s = store.get(); + s.token = token.to_string(); + store.set(s); + // 通知 ws 重连携带新 token + let _ = app.state::().tx.send(crate::ws::WsCmd::Reconnect); + } + } + Some(v) +} + +#[tauri::command] +pub async fn create_order(app: AppHandle, pack_id: String) -> Option { + let (url, token) = base(&app); + reqwest::Client::new() + .post(format!("{url}/v1/orders")) + .bearer_auth(token) + .json(&serde_json::json!({"pack_id": pack_id, "channel": "native"})) + .send() + .await + .ok()? + .json() + .await + .ok() +} + +#[tauri::command] +pub async fn order_status(app: AppHandle, order_id: String) -> Option { + get_json(&app, &format!("/v1/orders/{order_id}"), true).await +} + +// ─── 反馈(11E)────────────────────────────────────────────────────────────── + +#[derive(serde::Deserialize)] +pub struct FeedbackImage { + pub name: String, + /// base64(不含 data: 前缀) + pub data: String, +} + +fn image_mime(name: &str) -> &'static str { + let lower = name.to_ascii_lowercase(); + if lower.ends_with(".png") { + "image/png" + } else if lower.ends_with(".webp") { + "image/webp" + } else { + "image/jpeg" + } +} + +#[cfg(target_os = "macos")] +fn os_version() -> String { + std::process::Command::new("sw_vers") + .arg("-productVersion") + .output() + .ok() + .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string()) + .unwrap_or_default() +} + +#[cfg(not(target_os = "macos"))] +fn os_version() -> String { + String::new() +} + +/// multipart POST /v1/feedback:文字 + 图片(≤3 张 ≤5MB)+ 可选诊断信息。 +/// 错误以字符串 code 返回:NOT_LOGGED_IN / FEEDBACK_RATE_LIMITED / HTTP_xxx / 网络错误描述。 +#[tauri::command] +pub async fn submit_feedback( + app: AppHandle, + content: String, + images: Vec, + include_diagnostics: bool, +) -> Result { + let (url, token) = base(&app); + if token.is_empty() { + return Err("NOT_LOGGED_IN".into()); + } + let app_version = app.package_info().version.to_string(); + + let mut form = reqwest::multipart::Form::new().text("content", content); + for img in images.into_iter().take(3) { + use base64::Engine; + let bytes = base64::engine::general_purpose::STANDARD + .decode(img.data.as_bytes()) + .map_err(|e| e.to_string())?; + let part = reqwest::multipart::Part::bytes(bytes) + .mime_str(image_mime(&img.name)) + .map_err(|e| e.to_string())? + .file_name(img.name); + form = form.part("images[]", part); + } + if include_diagnostics { + let s = app.state::().get(); + let diag = json!({ + "app_version": app_version, + "platform": std::env::consts::OS, + "os_version": os_version(), + "device_id": s.device_id, + }); + form = form.text("diagnostics", diag.to_string()); + } + + let resp = reqwest::Client::new() + .post(format!("{url}/v1/feedback")) + .bearer_auth(token) + .header("X-Platform", std::env::consts::OS) + .header("X-App-Version", app_version) + .multipart(form) + .send() + .await + .map_err(|e| e.to_string())?; + match resp.status().as_u16() { + 200 => resp.json().await.map_err(|e| e.to_string()), + 429 => Err("FEEDBACK_RATE_LIMITED".into()), + s => Err(format!("HTTP_{s}")), + } +} + +// ─── 窗口控制 ──────────────────────────────────────────────────────────────── + +#[tauri::command] +pub fn open_settings(app: AppHandle) { + if let Some(w) = app.get_webview_window("settings") { + let _ = w.show(); + let _ = w.set_focus(); + } +} + +#[tauri::command] +pub fn open_login(app: AppHandle) { + if let Some(w) = app.get_webview_window("login") { + let _ = w.show(); + let _ = w.set_focus(); + } +} + +#[tauri::command] +pub fn close_login(app: AppHandle) { + if let Some(w) = app.get_webview_window("login") { + let _ = w.hide(); + } +} + +#[tauri::command] +pub fn open_feedback(app: AppHandle) { + if let Some(w) = app.get_webview_window("feedback") { + let _ = w.show(); + let _ = w.set_focus(); + } +} + +#[tauri::command] +pub fn close_feedback(app: AppHandle) { + if let Some(w) = app.get_webview_window("feedback") { + let _ = w.hide(); + } +} + +/// 完成首次启动引导(11F):置 onboarding_done 并关闭引导窗。 +#[tauri::command] +pub fn finish_onboarding(app: AppHandle) { + let store = app.state::(); + let mut s = store.get(); + if !s.onboarding_done { + s.onboarding_done = true; + store.set(s); + } + if let Some(w) = app.get_webview_window("onboarding") { + let _ = w.hide(); + } +} + +/// 检查更新:GET /v1/app/latest,有新版则打开下载页;返回结果供 UI 提示。 +#[tauri::command] +pub async fn check_update(app: AppHandle) -> Option { + let current = app.package_info().version.to_string(); + #[cfg(target_os = "macos")] + let platform = "mac"; + #[cfg(target_os = "windows")] + let platform = "win"; + #[cfg(all(not(target_os = "macos"), not(target_os = "windows")))] + let platform = "linux"; + let latest = get_json(&app, &format!("/v1/app/latest?platform={platform}"), false).await?; + let version = latest.get("version").and_then(|v| v.as_str()).unwrap_or_default(); + let dl = latest.get("url").and_then(|v| v.as_str()).unwrap_or_default(); + let available = !version.is_empty() && version != current; + if available && !dl.is_empty() { + open_external(dl); + } + Some(json!({"current": current, "latest": version, "update_available": available, "url": dl})) +} + +/// 用系统默认方式打开外部链接。 +fn open_external(url: &str) { + #[cfg(target_os = "macos")] + let _ = std::process::Command::new("open").arg(url).spawn(); + #[cfg(target_os = "windows")] + let _ = std::process::Command::new("cmd").args(["/C", "start", "", url]).spawn(); + #[cfg(all(not(target_os = "macos"), not(target_os = "windows")))] + let _ = std::process::Command::new("xdg-open").arg(url).spawn(); +} + +#[tauri::command] +pub fn quit_app(app: AppHandle) { + app.exit(0); +} diff --git a/desktop/src-tauri/src/audio.rs b/desktop/src-tauri/src/audio.rs new file mode 100644 index 0000000..173ef05 --- /dev/null +++ b/desktop/src-tauri/src/audio.rs @@ -0,0 +1,185 @@ +//! 音频采集:cpal 输入流 → 单声道 → 线性重采样 16kHz i16 → 100ms 帧(3200B)。 + +use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; +use std::sync::mpsc::Sender; + +pub const TARGET_RATE: u32 = 16000; +pub const FRAME_SAMPLES: usize = 1600; // 100ms @16k + +pub fn list_devices() -> Vec { + let host = cpal::default_host(); + host.input_devices() + .map(|it| it.filter_map(|d| d.name().ok()).collect()) + .unwrap_or_default() +} + +/// 触发麦克风授权(11F 引导页):短暂起一个输入流,首次会弹系统授权框。 +/// 返回 true 表示流可建立(设备可用 / 已授权或用户刚授权)。 +#[tauri::command] +pub async fn request_microphone() -> bool { + tauri::async_runtime::spawn_blocking(|| { + let (tx, rx) = std::sync::mpsc::channel::>(); + match start("", tx) { + Ok(capture) => { + std::thread::sleep(std::time::Duration::from_millis(300)); + drop(rx); + drop(capture); + true + } + Err(_) => false, + } + }) + .await + .unwrap_or(false) +} + +/// 采集句柄:drop 即停止。cpal::Stream 在 macOS 上非 Send, +/// 因此流由专用线程持有,句柄只保留停止信号端。 +pub struct Capture { + stop: Option>, +} + +impl Drop for Capture { + fn drop(&mut self) { + self.stop.take(); // 关闭通道 → 采集线程退出并销毁流 + } +} + +/// 启动采集;每凑满 100ms 帧经 `tx` 发出。 +pub fn start(device_name: &str, tx: Sender>) -> Result { + let (stop_tx, stop_rx) = std::sync::mpsc::channel::<()>(); + let (ready_tx, ready_rx) = std::sync::mpsc::channel::>(); + let device_name = device_name.to_string(); + + std::thread::spawn(move || { + let built = build_stream(&device_name, tx); + match built { + Ok(stream) => { + if let Err(e) = stream.play() { + let _ = ready_tx.send(Err(e.to_string())); + return; + } + let _ = ready_tx.send(Ok(())); + // 阻塞直至句柄 drop(发送端关闭 → recv Err) + let _ = stop_rx.recv(); + drop(stream); + } + Err(e) => { + let _ = ready_tx.send(Err(e)); + } + } + }); + + ready_rx + .recv() + .map_err(|_| "采集线程异常退出".to_string())??; + Ok(Capture { stop: Some(stop_tx) }) +} + +fn build_stream(device_name: &str, tx: Sender>) -> Result { + let host = cpal::default_host(); + let device = if device_name.is_empty() { + host.default_input_device() + } else { + host.input_devices() + .ok() + .and_then(|mut it| it.find(|d| d.name().map(|n| n == device_name).unwrap_or(false))) + .or_else(|| host.default_input_device()) + } + .ok_or("没有可用麦克风")?; + + let config = device.default_input_config().map_err(|e| e.to_string())?; + let src_rate = config.sample_rate().0; + let channels = config.channels() as usize; + let mut resampler = Resampler::new(src_rate, channels, tx); + + let err_fn = |e| log::error!("audio stream error: {e}"); + match config.sample_format() { + cpal::SampleFormat::F32 => device.build_input_stream( + &config.into(), + move |data: &[f32], _| resampler.push_f32(data), + err_fn, + None, + ), + cpal::SampleFormat::I16 => device.build_input_stream( + &config.into(), + move |data: &[i16], _| resampler.push_i16(data), + err_fn, + None, + ), + f => return Err(format!("不支持的采样格式: {f:?}")), + } + .map_err(|e| e.to_string()) +} + +/// 线性插值重采样 + 分帧。 +struct Resampler { + src_rate: u32, + channels: usize, + pos: f64, + step: f64, + prev: f32, + buf: Vec, + tx: Sender>, +} + +impl Resampler { + fn new(src_rate: u32, channels: usize, tx: Sender>) -> Self { + Self { + src_rate, + channels, + pos: 0.0, + step: src_rate as f64 / TARGET_RATE as f64, + prev: 0.0, + buf: Vec::with_capacity(FRAME_SAMPLES * 2), + tx, + } + } + + fn push_f32(&mut self, data: &[f32]) { + let mono: Vec = data + .chunks(self.channels) + .map(|c| c.iter().sum::() / self.channels as f32) + .collect(); + self.resample(&mono); + } + + fn push_i16(&mut self, data: &[i16]) { + let mono: Vec = data + .chunks(self.channels) + .map(|c| c.iter().map(|&s| s as f32 / 32768.0).sum::() / self.channels as f32) + .collect(); + self.resample(&mono); + } + + fn resample(&mut self, mono: &[f32]) { + if self.src_rate == TARGET_RATE { + for &s in mono { + self.emit(s); + } + return; + } + for &s in mono { + while self.pos < 1.0 { + let v = self.prev + (s - self.prev) * self.pos as f32; + self.emit(v); + self.pos += self.step; + } + self.pos -= 1.0; + self.prev = s; + } + } + + fn emit(&mut self, sample: f32) { + let v = (sample.clamp(-1.0, 1.0) * 32767.0) as i16; + self.buf.push(v); + if self.buf.len() >= FRAME_SAMPLES { + let frame: Vec = self.buf[..FRAME_SAMPLES] + .iter() + .flat_map(|s| s.to_le_bytes()) + .collect(); + self.buf.drain(..FRAME_SAMPLES); + let _ = self.tx.send(frame); + } + } +} diff --git a/desktop/src-tauri/src/dictation.rs b/desktop/src-tauri/src/dictation.rs new file mode 100644 index 0000000..93784e8 --- /dev/null +++ b/desktop/src-tauri/src/dictation.rs @@ -0,0 +1,218 @@ +//! 听写编排(push-to-talk 核心): +//! 快捷键按下 → 浮层弹出(光标附近,不抢焦点)→ 采集推流; +//! 松开 → stop → 等尾部 final → 注入 committed 文本 → 浮层淡出。 + +use parking_lot::Mutex; +use serde_json::{json, Value}; +use std::sync::mpsc; +use std::time::{Duration, Instant}; +use tauri::{AppHandle, Emitter, Manager}; + +#[derive(Default)] +pub struct DictationState { + inner: Mutex>, +} + +struct Session { + id: String, + capture: Option, + final_text: String, + partial_text: String, + started: Instant, + got_first_partial: bool, +} + +pub fn start(app: &AppHandle) { + let state = app.state::(); + if state.inner.lock().is_some() { + return; // 已在录音 + } + if crate::api::is_app_paused(app) { + return; // 已暂停使用 + } + let session_id = uuid::Uuid::new_v4().to_string(); + set_tray_tooltip(app, "dudu — 录音中"); + let _ = app.emit("hotkey", json!({"state": "down"})); + show_overlay(app); + + let ws = app.state::().tx.clone(); + let _ = ws.send(crate::ws::WsCmd::Start { + session_id: session_id.clone(), + }); + + // 音频帧经 std channel → 转发 tokio channel + let (tx, rx) = mpsc::channel::>(); + let mic = app.state::().get().mic; + let t0 = Instant::now(); + let capture = match crate::audio::start(&mic, tx) { + Ok(c) => { + let m = app.state::(); + m.record("audio.start_ms", json!({"ms": t0.elapsed().as_millis() as i64})); + Some(c) + } + Err(e) => { + log::error!("audio start failed: {e}"); + let _ = app.emit("asr", json!({"type":"error","code":"AUDIO","message": e})); + None + } + }; + { + let ws = ws.clone(); + std::thread::spawn(move || { + while let Ok(frame) = rx.recv() { + if ws.send(crate::ws::WsCmd::Audio(frame)).is_err() { + break; + } + } + }); + } + + *state.inner.lock() = Some(Session { + id: session_id, + capture, + final_text: String::new(), + partial_text: String::new(), + started: Instant::now(), + got_first_partial: false, + }); +} + +pub fn stop(app: &AppHandle, canceled: bool) { + let state = app.state::(); + let Some(mut sess) = state.inner.lock().take() else { + return; + }; + let _ = app.emit("hotkey", json!({"state": "up"})); + set_tray_tooltip(app, "dudu — 就绪"); + sess.capture.take(); // 停止采集 + + let ws = app.state::().tx.clone(); + let cmd = if canceled { + crate::ws::WsCmd::Cancel { session_id: sess.id.clone() } + } else { + crate::ws::WsCmd::Stop { session_id: sess.id.clone() } + }; + let _ = ws.send(cmd); + + let app = app.clone(); + let release_at = Instant::now(); + tauri::async_runtime::spawn(async move { + if !canceled { + // 等网关 flush 尾部 final(句间已实时累计,这里只兜尾部) + tokio::time::sleep(Duration::from_millis(350)).await; + let committed = { + let state = app.state::(); + let pending = state.inner.lock(); + drop(pending); + let buf = app.state::(); + buf.take() + }; + if !committed.is_empty() { + if !crate::inject::accessibility_ok() { + // 无辅助功能权限(macOS):注入必失败 → 浮层引导去授权(10E) + let _ = app.emit( + "asr", + json!({"type":"error","code":"NO_ACCESSIBILITY","message":"需要辅助功能权限"}), + ); + tokio::time::sleep(Duration::from_millis(2400)).await; // 让用户看清引导 + } else { + let t = committed.clone(); + let injected = + tauri::async_runtime::spawn_blocking(move || crate::inject::inject_text(&t)).await; + match injected { + Ok(Ok(())) => { + let m = app.state::(); + m.record( + "asr.release_to_commit_ms", + json!({"ms": release_at.elapsed().as_millis() as i64}), + ); + } + _ => { + log::error!("inject failed: {injected:?}"); + if !crate::inject::accessibility_ok() { + let _ = app.emit( + "asr", + json!({"type":"error","code":"NO_ACCESSIBILITY","message":"需要辅助功能权限"}), + ); + tokio::time::sleep(Duration::from_millis(2400)).await; + } + } + } + } + } + } else { + app.state::().take(); + } + hide_overlay(&app); + }); +} + +/// CommitBuffer 跨 ws 任务与停止流程共享的"待注入文本"。 +#[derive(Default)] +pub struct CommitBuffer { + text: Mutex<(String, String)>, // (final 累计, 最新 partial) +} + +impl CommitBuffer { + fn take(&self) -> String { + let mut t = self.text.lock(); + let committed = format!("{}{}", t.0, t.1); + *t = (String::new(), String::new()); + committed + } +} + +/// ws 下行回调:维护文本缓冲 + 首字延迟打点(在 ws.rs 收包处调用)。 +pub fn on_server_msg(app: &AppHandle, v: &Value) { + let buf = app.state::(); + match v.get("type").and_then(|t| t.as_str()) { + Some("partial") => { + if let Some(text) = v.get("text").and_then(|t| t.as_str()) { + buf.text.lock().1 = text.to_string(); + } + let state = app.state::(); + let mut guard = state.inner.lock(); + if let Some(sess) = guard.as_mut() { + if !sess.got_first_partial { + sess.got_first_partial = true; + let m = app.state::(); + m.record( + "asr.first_partial_ms", + json!({"ms": sess.started.elapsed().as_millis() as i64}), + ); + } + } + } + Some("final") => { + if let Some(text) = v.get("text").and_then(|t| t.as_str()) { + let mut t = buf.text.lock(); + t.0.push_str(text); + t.1.clear(); + } + } + _ => {} + } +} + +/// 托盘提示按录音状态切换(11B)。 +fn set_tray_tooltip(app: &AppHandle, text: &str) { + if let Some(tray) = app.tray_by_id("main") { + let _ = tray.set_tooltip(Some(text)); + } +} + +fn show_overlay(app: &AppHandle) { + if let Some(w) = app.get_webview_window("overlay") { + // 跟随光标:浮层出现在指针下方 24px,水平居中 + if let Ok(pos) = app.cursor_position() { + let _ = w.set_position(tauri::PhysicalPosition::new(pos.x - 180.0, pos.y + 24.0)); + } + let _ = w.show(); + } +} + +fn hide_overlay(app: &AppHandle) { + if let Some(w) = app.get_webview_window("overlay") { + let _ = w.hide(); + } +} diff --git a/desktop/src-tauri/src/inject.rs b/desktop/src-tauri/src/inject.rs new file mode 100644 index 0000000..b592c34 --- /dev/null +++ b/desktop/src-tauri/src/inject.rs @@ -0,0 +1,74 @@ +//! 文字注入(10E/10F):剪贴板写入 → 模拟粘贴(mac ⌘V / win Ctrl+V)→ 恢复剪贴板。 +//! 兼容性最好的方案;macOS 需辅助功能权限(首次启动引导授予)。 + +use enigo::{Direction, Enigo, Key, Keyboard, Settings as EnigoSettings}; +use std::{thread, time::Duration}; + +// ─── macOS 辅助功能权限(10E)──────────────────────────────────────────────── + +#[cfg(target_os = "macos")] +#[link(name = "ApplicationServices", kind = "framework")] +extern "C" { + /// 进程是否已被授予辅助功能权限(不弹系统提示)。 + fn AXIsProcessTrusted() -> u8; +} + +/// 是否已具备注入所需的辅助功能权限;非 macOS 恒为 true。 +pub fn accessibility_ok() -> bool { + #[cfg(target_os = "macos")] + { + unsafe { AXIsProcessTrusted() != 0 } + } + #[cfg(not(target_os = "macos"))] + { + true + } +} + +#[tauri::command] +pub fn check_accessibility() -> bool { + accessibility_ok() +} + +/// 打开系统设置的辅助功能面板(macOS);其他平台 no-op。 +#[tauri::command] +pub fn open_accessibility_settings() { + #[cfg(target_os = "macos")] + { + let _ = std::process::Command::new("open") + .arg("x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility") + .spawn(); + } +} + +pub fn inject_text(text: &str) -> Result<(), String> { + if text.is_empty() { + return Ok(()); + } + let mut clipboard = arboard::Clipboard::new().map_err(|e| e.to_string())?; + let saved = clipboard.get_text().ok(); + + clipboard.set_text(text).map_err(|e| e.to_string())?; + thread::sleep(Duration::from_millis(30)); // 等剪贴板生效 + + let mut enigo = Enigo::new(&EnigoSettings::default()).map_err(|e| e.to_string())?; + #[cfg(target_os = "macos")] + let modifier = Key::Meta; + #[cfg(not(target_os = "macos"))] + let modifier = Key::Control; + + enigo.key(modifier, Direction::Press).map_err(|e| e.to_string())?; + enigo + .key(Key::Unicode('v'), Direction::Click) + .map_err(|e| e.to_string())?; + enigo + .key(modifier, Direction::Release) + .map_err(|e| e.to_string())?; + + // 粘贴完成后恢复用户剪贴板 + thread::sleep(Duration::from_millis(120)); + if let Some(old) = saved { + let _ = clipboard.set_text(old); + } + Ok(()) +} diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs new file mode 100644 index 0000000..fe44b10 --- /dev/null +++ b/desktop/src-tauri/src/lib.rs @@ -0,0 +1,155 @@ +//! dudu 桌面端(Tauri 2)。模块划分见 doc/frontend-design.html 4.1。 + +pub mod api; +pub mod audio; +pub mod dictation; +pub mod inject; +pub mod metrics; +pub mod settings; +pub mod ws; + +use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent}; +use tauri::Manager; +use tauri_plugin_global_shortcut::{Shortcut, ShortcutState}; + +/// 托盘图标 rect → 菜单窗口左上角物理坐标(图标正下方、水平居中)。 +fn tray_menu_position(rect: &tauri::Rect, scale: f64, win_width: f64) -> (f64, f64) { + let (x, y) = match rect.position { + tauri::Position::Physical(p) => (p.x as f64, p.y as f64), + tauri::Position::Logical(p) => (p.x * scale, p.y * scale), + }; + let (w, h) = match rect.size { + tauri::Size::Physical(s) => (s.width as f64, s.height as f64), + tauri::Size::Logical(s) => (s.width * scale, s.height * scale), + }; + (x + w / 2.0 - win_width / 2.0, y + h + 4.0 * scale) +} + +/// 左键点击托盘图标 → 在图标位置弹出 / 收起自绘菜单窗(11B)。 +fn toggle_tray_menu(app: &tauri::AppHandle, rect: &tauri::Rect) { + let Some(w) = app.get_webview_window("tray") else { + return; + }; + if w.is_visible().unwrap_or(false) { + let _ = w.hide(); + return; + } + let scale = w.scale_factor().unwrap_or(1.0); + let win_width = w + .outer_size() + .map(|s| s.width as f64) + .unwrap_or(248.0 * scale); + let (x, y) = tray_menu_position(rect, scale, win_width); + let _ = w.set_position(tauri::PhysicalPosition::new(x, y)); + let _ = w.show(); + let _ = w.set_focus(); // 获焦后才能在失焦时自动收起 +} + +pub fn run() { + env_logger::init(); + tauri::Builder::default() + .plugin( + tauri_plugin_global_shortcut::Builder::new() + .with_handler(|app, _shortcut: &Shortcut, event| { + match event.state() { + ShortcutState::Pressed => dictation::start(app), + ShortcutState::Released => dictation::stop(app, false), + } + }) + .build(), + ) + .on_window_event(|window, event| match event { + // 托盘菜单窗失焦自动收起 + tauri::WindowEvent::Focused(false) if window.label() == "tray" => { + let _ = window.hide(); + } + // 常驻托盘:功能窗口"关闭"仅隐藏,应用不退出 + tauri::WindowEvent::CloseRequested { api, .. } => { + match window.label() { + "settings" | "login" | "feedback" => { + api.prevent_close(); + let _ = window.hide(); + } + "onboarding" => { + // 关闭引导窗 = 完成引导(11F) + api.prevent_close(); + api::finish_onboarding(window.app_handle().clone()); + } + _ => {} + } + } + _ => {} + }) + .setup(|app| { + let handle = app.handle().clone(); + // 设置 → 管理状态 + app.manage(settings::SettingsStore::load(&handle)); + app.manage(dictation::DictationState::default()); + app.manage(dictation::CommitBuffer::default()); + app.manage(api::Paused::default()); + // WS 预连接 + 打点队列 + app.manage(ws::spawn(handle.clone())); + app.manage(metrics::spawn(handle.clone())); + // 系统托盘(11B):左键弹出自绘菜单窗,tooltip 随录音状态切换 + let icon = app + .default_window_icon() + .cloned() + .expect("missing app icon"); + TrayIconBuilder::with_id("main") + .icon(icon) + .tooltip("dudu — 就绪") + .on_tray_icon_event(|tray, event| { + if let TrayIconEvent::Click { + button: MouseButton::Left, + button_state: MouseButtonState::Up, + rect, + .. + } = event + { + toggle_tray_menu(tray.app_handle(), &rect); + } + }) + .build(app)?; + // 注册 push-to-talk 快捷键 + let s = app.state::().get(); + use tauri_plugin_global_shortcut::GlobalShortcutExt; + if let Err(e) = app.global_shortcut().register(s.hotkey.as_str()) { + log::error!("register hotkey failed: {e}"); + } + // 首次启动 → 引导窗(11F);之后均静默启动(仅托盘常驻) + if !s.onboarding_done { + if let Some(w) = app.get_webview_window("onboarding") { + let _ = w.show(); + let _ = w.set_focus(); + } + } + Ok(()) + }) + .invoke_handler(tauri::generate_handler![ + settings::get_settings, + settings::set_settings, + settings::list_mic_devices, + audio::request_microphone, + inject::check_accessibility, + inject::open_accessibility_settings, + api::fetch_me, + api::fetch_packs, + api::login_qr_create, + api::login_qr_poll, + api::create_order, + api::order_status, + api::submit_feedback, + api::is_paused, + api::toggle_pause, + api::open_settings, + api::open_login, + api::close_login, + api::open_feedback, + api::close_feedback, + api::finish_onboarding, + api::check_update, + api::quit_app, + ]) + .run(tauri::generate_context!()) + .expect("error while running dudu"); +} diff --git a/desktop/src-tauri/src/main.rs b/desktop/src-tauri/src/main.rs new file mode 100644 index 0000000..ef09328 --- /dev/null +++ b/desktop/src-tauri/src/main.rs @@ -0,0 +1,5 @@ +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +fn main() { + dudu_desktop_lib::run() +} diff --git a/desktop/src-tauri/src/metrics.rs b/desktop/src-tauri/src/metrics.rs new file mode 100644 index 0000000..ebd12f1 --- /dev/null +++ b/desktop/src-tauri/src/metrics.rs @@ -0,0 +1,67 @@ +//! 客户端打点(10H):内存队列,满 20 条或 60s 批量上报;失败丢弃不影响功能。 + +use parking_lot::Mutex; +use serde_json::{json, Value}; +use std::sync::Arc; +use std::time::Duration; +use tauri::{AppHandle, Manager}; + +#[derive(Clone)] +pub struct Metrics { + queue: Arc>>, +} + +impl Metrics { + pub fn record(&self, event: &str, props: Value) { + let mut q = self.queue.lock(); + q.push(json!({ + "event": event, + "props": props, + "client_ts": std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH).map(|d| d.as_millis() as i64).unwrap_or(0), + })); + if q.len() > 1000 { + let overflow = q.len() - 1000; + q.drain(..overflow); // 上限丢最旧 + } + } +} + +pub fn spawn(app: AppHandle) -> Metrics { + let m = Metrics { + queue: Arc::new(Mutex::new(Vec::new())), + }; + let queue = m.queue.clone(); + tauri::async_runtime::spawn(async move { + let client = reqwest::Client::new(); + let mut tick = tokio::time::interval(Duration::from_secs(10)); + loop { + tick.tick().await; + let (events, url, device_id) = { + let mut q = queue.lock(); + if q.is_empty() || (q.len() < 20 && tickcount_skip(&q)) { + continue; + } + let events: Vec = q.drain(..).collect(); + let s = app.state::().get(); + (events, format!("{}/v1/metrics/batch", s.server_url), s.device_id) + }; + let body = json!({ + "device_id": device_id, + "platform": std::env::consts::OS, + "app_version": env!("CARGO_PKG_VERSION"), + "os_version": "", + "events": events, + }); + let _ = client.post(&url).json(&body).send().await; // 失败即丢弃 + } + }); + m +} + +// 不满 20 条时按 60s 节奏(tick 10s × 6 次取模近似)上报;保持实现极简。 +fn tickcount_skip(_q: &[Value]) -> bool { + use std::sync::atomic::{AtomicU64, Ordering}; + static N: AtomicU64 = AtomicU64::new(0); + N.fetch_add(1, Ordering::Relaxed) % 6 != 0 +} diff --git a/desktop/src-tauri/src/settings.rs b/desktop/src-tauri/src/settings.rs new file mode 100644 index 0000000..322e246 --- /dev/null +++ b/desktop/src-tauri/src/settings.rs @@ -0,0 +1,94 @@ +//! 应用设置:本地 JSON 持久化(MVP;token 后续迁系统钥匙串)。 + +use parking_lot::Mutex; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; +use tauri::Manager; + +#[derive(Clone, Serialize, Deserialize)] +#[serde(default)] +pub struct Settings { + pub hotkey: String, + pub mic: String, + pub sound: bool, + pub autostart: bool, + pub theme: String, + pub server_url: String, + pub token: String, + pub device_id: String, + /// 首次启动引导是否已完成(11F)。 + pub onboarding_done: bool, +} + +impl Default for Settings { + fn default() -> Self { + Self { + hotkey: "CmdOrCtrl+Shift+Space".into(), + mic: String::new(), + sound: false, + autostart: true, + theme: "system".into(), + server_url: "http://localhost:8080".into(), + token: String::new(), + device_id: uuid::Uuid::new_v4().to_string(), + onboarding_done: false, + } + } +} + +pub struct SettingsStore { + path: PathBuf, + pub current: Mutex, +} + +impl SettingsStore { + pub fn load(app: &tauri::AppHandle) -> Self { + let dir = app + .path() + .app_config_dir() + .unwrap_or_else(|_| std::env::temp_dir()); + let _ = std::fs::create_dir_all(&dir); + let path = dir.join("settings.json"); + let current = std::fs::read(&path) + .ok() + .and_then(|b| serde_json::from_slice(&b).ok()) + .unwrap_or_default(); + let store = Self { + path, + current: Mutex::new(current), + }; + store.save(); // 首次生成 device_id 后立即落盘 + store + } + + pub fn save(&self) { + let s = self.current.lock().clone(); + if let Ok(b) = serde_json::to_vec_pretty(&s) { + let _ = std::fs::write(&self.path, b); + } + } + + pub fn get(&self) -> Settings { + self.current.lock().clone() + } + + pub fn set(&self, s: Settings) { + *self.current.lock() = s; + self.save(); + } +} + +#[tauri::command] +pub fn get_settings(store: tauri::State) -> Settings { + store.get() +} + +#[tauri::command] +pub fn set_settings(store: tauri::State, settings: Settings) { + store.set(settings); +} + +#[tauri::command] +pub fn list_mic_devices() -> Vec { + crate::audio::list_devices() +} diff --git a/desktop/src-tauri/src/ws.rs b/desktop/src-tauri/src/ws.rs new file mode 100644 index 0000000..31f9c9f --- /dev/null +++ b/desktop/src-tauri/src/ws.rs @@ -0,0 +1,125 @@ +//! WS 客户端(10C):预连接 + 心跳 + 指数退避重连; +//! 上行音频二进制帧 / 控制 JSON,下行 ServerMsg → emit("asr") + 维护听写文本状态。 + +use futures_util::{SinkExt, StreamExt}; +use serde_json::json; +use std::time::Duration; +use tauri::{AppHandle, Emitter, Manager}; +use tokio::sync::mpsc; +use tokio_tungstenite::tungstenite::client::IntoClientRequest; +use tokio_tungstenite::tungstenite::Message; + +#[derive(Debug)] +pub enum WsCmd { + Start { session_id: String }, + Audio(Vec), + Stop { session_id: String }, + Cancel { session_id: String }, + Reconnect, // 设置变更(token/服务器)后强制重连 +} + +#[derive(Clone)] +pub struct WsHandle { + pub tx: mpsc::UnboundedSender, +} + +/// 启动常驻连接任务(应用启动即预连接,按键时零握手)。 +pub fn spawn(app: AppHandle) -> WsHandle { + let (tx, mut rx) = mpsc::unbounded_channel::(); + tauri::async_runtime::spawn(async move { + let mut backoff = Duration::from_millis(500); + loop { + let (url, token, device_id) = { + let store = app.state::(); + let s = store.get(); + let ws_url = s.server_url.replace("http://", "ws://").replace("https://", "wss://"); + (format!("{ws_url}/v1/asr/stream"), s.token, s.device_id) + }; + if token.is_empty() { + // 未登录:等待命令但丢弃音频,定期重查 + tokio::time::sleep(Duration::from_secs(2)).await; + while let Ok(cmd) = rx.try_recv() { + drop(cmd); + } + continue; + } + + let mut req = match url.clone().into_client_request() { + Ok(r) => r, + Err(e) => { + log::error!("bad ws url: {e}"); + tokio::time::sleep(Duration::from_secs(5)).await; + continue; + } + }; + req.headers_mut().insert( + "Authorization", + format!("Bearer {token}").parse().unwrap(), + ); + req.headers_mut() + .insert("X-Device-ID", device_id.parse().unwrap()); + + let conn = tokio_tungstenite::connect_async(req).await; + let (mut sink, mut stream) = match conn { + Ok((ws, _)) => { + log::info!("ws connected"); + backoff = Duration::from_millis(500); + ws.split() + } + Err(e) => { + log::warn!("ws connect failed: {e}; retry in {backoff:?}"); + tokio::time::sleep(backoff).await; + backoff = (backoff * 2).min(Duration::from_secs(8)); + continue; + } + }; + + let mut ping = tokio::time::interval(Duration::from_secs(30)); + 'conn: loop { + tokio::select! { + cmd = rx.recv() => { + let Some(cmd) = cmd else { return }; + let msg = match cmd { + WsCmd::Start { session_id } => Message::Text( + json!({"type":"start","session_id":session_id,"sample_rate":16000,"format":"pcm16"}).to_string()), + WsCmd::Audio(frame) => Message::Binary(frame), + WsCmd::Stop { session_id } => Message::Text( + json!({"type":"stop","session_id":session_id}).to_string()), + WsCmd::Cancel { session_id } => Message::Text( + json!({"type":"cancel","session_id":session_id}).to_string()), + WsCmd::Reconnect => break 'conn, + }; + if sink.send(msg).await.is_err() { + let _ = app.emit("asr", json!({"type":"error","code":"NETWORK"})); + break 'conn; + } + } + incoming = stream.next() => { + match incoming { + Some(Ok(Message::Text(text))) => { + if let Ok(v) = serde_json::from_str::(&text) { + crate::dictation::on_server_msg(&app, &v); + let _ = app.emit("asr", v); + } + } + Some(Ok(Message::Ping(p))) => { let _ = sink.send(Message::Pong(p)).await; } + Some(Ok(_)) => {} + Some(Err(e)) => { + log::warn!("ws read error: {e}"); + let _ = app.emit("asr", json!({"type":"error","code":"NETWORK"})); + break 'conn; + } + None => break 'conn, + } + } + _ = ping.tick() => { + if sink.send(Message::Ping(Vec::new())).await.is_err() { break 'conn; } + } + } + } + tokio::time::sleep(backoff).await; + backoff = (backoff * 2).min(Duration::from_secs(8)); + } + }); + WsHandle { tx } +} diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json new file mode 100644 index 0000000..526593d --- /dev/null +++ b/desktop/src-tauri/tauri.conf.json @@ -0,0 +1,90 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "dudu", + "version": "0.1.0", + "identifier": "app.dudu.desktop", + "build": { + "beforeDevCommand": "npm run dev", + "devUrl": "http://localhost:5181", + "beforeBuildCommand": "npm run build", + "frontendDist": "../dist" + }, + "app": { + "windows": [ + { + "label": "settings", + "title": "dudu 设置", + "url": "index.html", + "width": 440, + "height": 560, + "resizable": false, + "visible": false + }, + { + "label": "overlay", + "title": "dudu", + "url": "overlay.html", + "width": 360, + "height": 140, + "visible": false, + "transparent": true, + "decorations": false, + "alwaysOnTop": true, + "focus": false, + "skipTaskbar": true, + "shadow": false + }, + { + "label": "login", + "title": "dudu — 登录", + "url": "login.html", + "width": 400, + "height": 520, + "resizable": false, + "visible": false + }, + { + "label": "tray", + "title": "dudu", + "url": "tray.html", + "width": 248, + "height": 300, + "resizable": false, + "visible": false, + "transparent": true, + "decorations": false, + "alwaysOnTop": true, + "skipTaskbar": true, + "shadow": false + }, + { + "label": "feedback", + "title": "反馈问题", + "url": "feedback.html", + "width": 460, + "height": 560, + "resizable": false, + "center": true, + "visible": false + }, + { + "label": "onboarding", + "title": "欢迎使用 dudu", + "url": "onboarding.html", + "width": 560, + "height": 480, + "resizable": false, + "center": true, + "visible": false + } + ], + "security": { + "csp": null + } + }, + "bundle": { + "active": true, + "targets": "all", + "icon": ["icons/icon.png"] + } +} diff --git a/desktop/src/feedback/main.jsx b/desktop/src/feedback/main.jsx new file mode 100644 index 0000000..642dde0 --- /dev/null +++ b/desktop/src/feedback/main.jsx @@ -0,0 +1,237 @@ +// 反馈窗口(11E):文字 + 图片(粘贴 / 拖拽 / 选择,≤3 张 ≤5MB)+ 诊断信息开关。 +// 规格见 doc/frontend-design.html 第八章;提交走 multipart POST /v1/feedback。 +import { useEffect, useRef, useState } from 'react'; +import { createRoot } from 'react-dom/client'; +import '@dudu/design/styles.css'; +import { Button } from '@dudu/design/components/core/Button'; +import { Switch } from '@dudu/design/components/forms/Switch'; +import { SettingRow } from '@dudu/design/components/forms/SettingRow'; +import { invoke } from '../shared/tauri'; +import { setThemePref } from '../shared/theme'; + +const MAX_IMAGES = 3; +const MAX_BYTES = 5 * 1024 * 1024; +const MAX_LEN = 1000; +const OK_TYPES = ['image/jpeg', 'image/png', 'image/webp']; + +const ERR_TEXT = { + NOT_LOGGED_IN: '先登录再反馈', + FEEDBACK_RATE_LIMITED: '今天反馈次数已达上限', +}; + +// File → { name, dataUrl, base64 } +function readImage(file) { + return new Promise((resolve) => { + const reader = new FileReader(); + reader.onload = () => { + const dataUrl = reader.result; + resolve({ name: file.name || 'screenshot.png', dataUrl, base64: dataUrl.split(',')[1] }); + }; + reader.onerror = () => resolve(null); + reader.readAsDataURL(file); + }); +} + +function Thumb({ img, onRemove }) { + return ( +
+ {img.name} + +
+ ); +} + +function AddBox({ onClick, dragging }) { + const [hover, setHover] = useState(false); + const on = hover || dragging; + return ( + + ); +} + +function SuccessView({ onAgain }) { + return ( +
+ + + +
已收到 · 谢谢你
+ +
+ ); +} + +function FeedbackApp() { + const [content, setContent] = useState(''); + const [images, setImages] = useState([]); + const [diag, setDiag] = useState(true); + const [submitting, setSubmitting] = useState(false); + const [done, setDone] = useState(false); + const [err, setErr] = useState(''); + const [dragging, setDragging] = useState(false); + const [loggedIn, setLoggedIn] = useState(true); + const fileRef = useRef(null); + const closeTimer = useRef(null); + + const refreshLogin = () => { + invoke('get_settings').then((s) => { + if (s) { setThemePref(s.theme); setLoggedIn(!!s.token); } + }); + }; + useEffect(() => { + refreshLogin(); + window.addEventListener('focus', refreshLogin); + return () => window.removeEventListener('focus', refreshLogin); + }, []); + + const addFiles = async (files) => { + setErr(''); + const list = Array.from(files).filter((f) => OK_TYPES.includes(f.type)); + if (!list.length) return; + const room = MAX_IMAGES - images.length; + if (room <= 0) { setErr(`最多 ${MAX_IMAGES} 张图片`); return; } + const next = []; + for (const f of list.slice(0, room)) { + if (f.size > MAX_BYTES) { setErr('单张图片不超过 5MB'); continue; } + const img = await readImage(f); + if (img) next.push(img); + } + if (next.length) setImages((cur) => [...cur, ...next].slice(0, MAX_IMAGES)); + }; + + // 粘贴截图(Cmd+V) + useEffect(() => { + const onPaste = (e) => { + const files = Array.from(e.clipboardData?.items || []) + .filter((it) => it.kind === 'file').map((it) => it.getAsFile()).filter(Boolean); + if (files.length) addFiles(files); + }; + document.addEventListener('paste', onPaste); + return () => document.removeEventListener('paste', onPaste); + }); + + const submit = async () => { + setSubmitting(true); + setErr(''); + try { + const r = await invoke('submit_feedback', { + content: content.trim(), + images: images.map((i) => ({ name: i.name, data: i.base64 })), + includeDiagnostics: diag, + }); + if (r && r.feedback_id) { + setDone(true); + setContent(''); setImages([]); + closeTimer.current = setTimeout(() => invoke('close_feedback'), 1800); + } else { + setErr('提交失败,请稍后再试'); + } + } catch (e) { + setErr(ERR_TEXT[String(e)] || '提交失败,请稍后再试'); + } finally { + setSubmitting(false); + } + }; + + const frame = (children) => ( +
{ e.preventDefault(); setDragging(true); }} + onDragLeave={() => setDragging(false)} + onDrop={(e) => { e.preventDefault(); setDragging(false); addFiles(e.dataTransfer.files); }} + style={{ + fontFamily: 'var(--font-sans)', background: 'var(--bg-app)', minHeight: '100vh', + boxSizing: 'border-box', padding: '20px 22px', display: 'flex', flexDirection: 'column', gap: 12, + }}> + {children} +
+ ); + + if (done) { + return frame( { clearTimeout(closeTimer.current); setDone(false); }} />); + } + + if (!loggedIn) { + return frame( +
+
先登录再反馈
+
登录后我们才能跟进你的问题
+ +
, + ); + } + + const count = [...content].length; + return frame( + <> +
告诉我们哪里不对
+
+ + + + +
+ + + + + \ No newline at end of file diff --git a/todo/for-me.html b/todo/for-me.html new file mode 100644 index 0000000..37b6fbd --- /dev/null +++ b/todo/for-me.html @@ -0,0 +1,432 @@ + + + + + +dudu · 需要你做的事 + + + + +
+

需要你做的事

+

dudu 项目 · 只有你能办的事项清单 · 勾选状态保存在本地浏览器 · 更新于 2026-06-11

+
+
0 / 0 已完成
+
+
+
+ +
+ + +
🔴 现在就做 — 直接阻塞开发,每项 ≤ 15 分钟3 项
+ +
+
+ +
+
iOS spike 真机验证:键盘扩展能否录音
+
+ 约 5 分钟 + 阻塞 iOS 全线(14D 技术路线) + 看板 1A +
+
+ +
+
+
为什么:iOS 键盘扩展内能否直接录音决定 iPhone/iPad 的产品体验走"键盘内主路径"还是"deep link 降级方案"。工程我已写好并通过模拟器编译,只缺真机结论。
+
流程
+
    +
  1. 终端执行 open ios/spike-kb-mic/DuduKbMicSpike.xcodeproj没有付费开发者账号也行:用免费 Apple ID 个人签名(7 天有效,spike 够用)
  2. +
  3. Xcode 中给 App 和 KeyboardExt 两个 target 分别设置签名Signing & Capabilities → Team 选你的 Apple ID;Bundle ID 冲突就在前面加自己的前缀
  4. +
  5. 选 App scheme + 你的 iPhone(数据线连接 + 信任电脑),⌘R 安装运行
  6. +
  7. 手机上打开 App → 点「测试 App 内录音(对照组)」→ 允许麦克风权限关键一步:键盘扩展自己弹不出权限框,必须靠容器 App 先拿到授权
  8. +
  9. 设置 → 通用 → 键盘 → 添加新键盘 → DuduKbMicSpike → 点进去打开「允许完全访问」
  10. +
  11. 打开备忘录,长按地球键切到 DuduKbMicSpike 键盘 → 点「测试键盘内录音(1 秒)」
  12. +
  13. 把键盘上显示的结果原文发给 Claude「✅ 收到 N 帧」= 主路径可行 |「❌ 0 帧 / 报错」= 走降级方案
  14. +
+
交付:键盘上显示的结果文字(截图也行)
+
+
+ +
+
+ +
+
开通阿里云百炼,拿 DashScope API Key
+
+ 约 15 分钟 + 阻塞真实语音识别(6B/9A/12B 延迟验收) + 看板 6B +
+
+ +
+
+
为什么:后端 gummy provider 代码已写好,全链路目前跑的是 mock 假识别。拿到 Key 后改一个环境变量即切真实大模型,桌面端就能说真话出真字。
+
流程
+
    +
  1. 登录阿里云 → 搜「百炼」或直达 bailian.console.aliyun.com → 同意开通服务新用户一般送免费额度;gummy-realtime 按音频时长计费,先确认下价格页
  2. +
  3. 控制台右上角「API-KEY 管理」→ 创建 API-KEY(sk- 开头)
  4. +
  5. 存入 Bitwarden:rbw add dashscope-api-key(密码字段填 Key)按全局规则不放明文文件/环境变量
  6. +
  7. 告诉 Claude「key 已就绪,条目名 dashscope-api-key」
  8. +
+
交付:Bitwarden 条目名(不要直接发 Key 内容)
+
+
+ +
+
+ +
+
桌面端首次联调:在你的 Mac 上跑起来
+
+ 约 10 分钟 + 阻塞 12A 全链路联调(无头环境验证不了快捷键/注入) + 看板 12A +
+
+ +
+
+
为什么:快捷键监听、麦克风采集、文字注入、浮层不抢焦点这四件事只有真实桌面环境能验证。我能改代码、看日志,但按键和说话得你来。
+
流程
+
    +
  1. 起后端(终端 1):cd server && docker compose up -d && go run ./cmd/server确认输出 dudu server listening;当前 ASR_PROVIDER=mock,说什么都会出预置句子,属正常
  2. +
  3. 起桌面端(终端 2):cd desktop && npm run tauri dev首次会编译 Rust,几分钟;之后秒起
  4. +
  5. 系统弹窗依次授予:麦克风权限辅助功能权限(系统设置 → 隐私与安全性 → 辅助功能,勾选 dudu/终端)
  6. +
  7. 设置窗口里点「微信登录」走 mock 扫码流程(开发期任意 code 都能登录)
  8. +
  9. 打开备忘录,光标点进正文,按住 ⌘⇧Space 说一句话再松开
  10. +
  11. 预期:浮层弹出「聆听中」→ 流式出字 → 松开后文字注入备忘录任何一步不符合预期,把两个终端的报错输出发给 Claude
  12. +
+
交付:「跑通了」或具体卡在哪一步 + 终端报错
+
+
+ + +
🟠 本周启动 — 外部审核周期长,越早提交越好4 项
+ +
+
+ +
+
购买域名 + ICP 备案
+
+ 操作 1 小时 · 审核 1-3 周 + 微信开放平台回调域名、支付回调、宣传页上线的共同前置 + 前置于 2A/2B +
+
+ +
+
+
为什么:微信「网站应用」的授权回调域名必须是已备案域名,支付回调和官网同理。备案周期 1-3 周,是整条微信链路的最长前置,必须最先动。
+
流程
+
    +
  1. 在阿里云万网查询并购买域名(如 duduime.com / dudu.app 类,注意 .app 等部分后缀不支持备案,选 .com/.cn 稳妥
  2. +
  3. 域名实名认证(企业主体,需营业执照)
  4. +
  5. 阿里云 ICP 代备案系统提交:营业执照、法人身份证、域名证书、网站信息(名称写「dudu 语音输入法官网」类)需要一台阿里云服务器/轻量应用服务器作为备案载体——顺手买一台最低配 ECS,之后部署后端也用它
  6. +
  7. 等管局审核(1-3 周),期间会有核验电话/短信
  8. +
  9. 备案号下来后告诉 Claude → 我更新宣传页页脚 + 配置部署
  10. +
+
交付:域名 + 备案号 + ECS 实例信息
+
+
+ +
+
+ +
+
微信开放平台:账号认证 + 创建两个应用
+
+ 材料半天 · 审核约 7 个工作日 + 阻塞真实微信登录(桌面扫码 + 移动端一键登录) + 看板 2A +
+
+ +
+
+
为什么:当前登录走 mock。桌面扫码用「网站应用」,手机 App 用「移动应用」,两个都要建,靠 unionid 把同一用户归一(后端已按此实现)。
+
流程
+
    +
  1. 注册 open.weixin.qq.com 账号(企业邮箱)→ 完成开发者资质认证(¥300/年,需营业执照、对公打款验证)
  2. +
  3. 创建网站应用:名称「dudu 语音输入」、官网地址、授权回调域名填备案好的域名依赖上一项的备案域名;审核约 7 个工作日
  4. +
  5. 创建移动应用:iOS 填 Bundle ID(建议 app.dudu.ime)、Android 填包名 + 签名 MD5Android 签名:先让 Claude 生成正式签名 keystore,再取 MD5 填入
  6. +
  7. 两个应用审核通过后,把 4 个凭证存入 Bitwarden:rbw add wechat-web-appidwechat-web-secretwechat-mobile-appidwechat-mobile-secret
  8. +
  9. 告诉 Claude「微信凭证就绪」→ 我替换 mock 实现并联调真实扫码
  10. +
+
交付:Bitwarden 4 个条目名 + unionid 是否已开通的确认
+
+
+ +
+
+ +
+
微信支付商户号 + API 证书配置
+
+ 材料半天 · 审核 1-5 个工作日 + 阻塞真实收款(7D 真实支付联调) + 看板 2B +
+
+ +
+
+
为什么:订单/入账/对账逻辑已完成并通过测试,差真实商户号验签收款。Native(桌面扫码)和 APP(移动收银台)两个支付产品都要开。
+
流程
+
    +
  1. pay.weixin.qq.com 申请商户号:营业执照、法人身份证、对公账户、经营信息(类目选软件/SaaS 类)
  2. +
  3. 审核通过后做对公账户打款验证、签署协议
  4. +
  5. 产品中心开通 Native 支付APP 支付 两个产品
  6. +
  7. 账户中心 → API 安全:设置 APIv3 密钥(32 位)、申请并下载商户 API 证书(apiclient_key.pem)
  8. +
  9. 产品中心 → AppID 账号管理:绑定开放平台的两个 AppID(关键,不绑无法下单)
  10. +
  11. 入 Bitwarden:rbw add wxpay-mchidwxpay-apiv3-keywxpay-cert-serial、私钥 PEM 存 wxpay-private-key回调地址我来配(需要备案域名 + HTTPS)
  12. +
  13. 告诉 Claude「商户号就绪」→ 我接 wechatpay-go 真实实现,先 1 分钱真实订单联调
  14. +
+
交付:Bitwarden 4 个条目名 + 已绑定 AppID 的确认
+
+
+ +
+
+ +
+
软件著作权申请
+
+ 材料 1 小时 · 登记 30-60 个工作日 + 阻塞应用商店上架(华为/小米等安卓市场硬性要求) + 看板 2C +
+
+ +
+
+
为什么:国内安卓应用市场上架要求软著证书,周期最长(30-60 个工作日),不卡开发但卡发布,宜早提交。
+
流程
+
    +
  1. 让 Claude 准备材料:源代码文档(前后各 30 页,每页 50 行)+ 软件说明书代码已有,说一声我就导出整理好
  2. +
  3. 中国版权保护中心 ccopyright.com.cn 注册账号(企业主体)
  4. +
  5. 在线填报:软件全称(如「嘟嘟语音输入法软件 V1.0」)、开发完成日期、开发方式(独立开发)等
  6. +
  7. 上传材料 + 邮寄纸质件(按系统提示);官方登记免费,着急可走代理加急(数百到千元)
  8. +
  9. 收到受理通知 → 等证书(电子版即可用于上架)
  10. +
+
交付:受理号;证书下来后存档
+
+
+ + +
⚪ 稍后 — 不阻塞当前开发,到节点再做3 项
+ +
+
+ +
+
Apple Developer Program(¥688/年)
+
+ 申请 1-2 天 + iOS 正式开发与上架前 +
+
+ +
+
+
为什么:spike 用免费签名即可,但正式开发需要它——TestFlight 内测、微信 SDK 的 Universal Link、App Group 正式配置、App Store 上架都依赖。iOS 主线(#14)开工前办好即可。
+
流程
+
    +
  1. 决定主体:个人账号(快)或公司账号(需邓白氏编码 D-U-N-S,多 1-2 周)付费软件 + 微信支付建议公司主体,与微信/商户号主体一致
  2. +
  3. developer.apple.com 注册并缴费
  4. +
  5. 开通后告诉 Claude:我来配 App ID(app.dudu.ime)、App Group、键盘扩展 entitlements
  6. +
+
交付:账号就绪通知 + Team ID
+
+
+ +
+
+ +
+
生产环境资源开通(阿里云)
+
+ 约 1 小时 + 内测部署前 +
+
+ +
+
+
为什么:内测前需要一套生产环境。备案时买的 ECS 可先兼任,正式内测建议按下面清单补齐(都选与百炼同地域,降低识别链路延迟)。
+
流程
+
    +
  1. ECS(备案那台可复用,2C4G 起步)+ 绑定域名 + HTTPS 证书(阿里云免费 DV 证书)
  2. +
  3. RDS PostgreSQL 最低配 + Redis 社区版最低配(或先单机 Docker 跑,量起来再上托管)
  4. +
  5. OSS Bucket(反馈图片,私有读写)+ 创建子账号 AK 存入 Bitwarden:rbw add aliyun-oss-ak
  6. +
  7. 告诉 Claude → 我写部署脚本(Docker Compose / systemd)+ 上线宣传页和后端
  8. +
+
交付:各资源连接信息(密钥走 Bitwarden)
+
+
+ +
+
+ +
+
验收看板上的「待验收」条目(持续)
+
+ 随时 · 每条 1 分钟 + 当前 6 条待验收 +
+
+ +
+
+
为什么:按工作流约定,accepted 状态只有你能标。当前待验收:#3 协议与基建、#4 数据层、#5 认证、#7 计费配额、#8 用户与周边、#15 宣传页。
+
流程
+
    +
  1. 打开看板 todo/todo.html 浏览「待验收」分组,或直接看交付物(文档/宣传页/测试输出)
  2. +
  3. 认可 → 对 Claude 说 /todo done 3(逐条);或一句话「3 4 5 7 8 15 都验收」
  4. +
  5. 不认可 → /todo reject <id> <原因>,条目自动退回并升为最高优先级
  6. +
+
交付:验收或拒绝的指令
+
+
+ +
+ +
+ + + + diff --git a/todo/todo.html b/todo/todo.html new file mode 100644 index 0000000..99b74b2 --- /dev/null +++ b/todo/todo.html @@ -0,0 +1,1404 @@ + + + + + +dudu — 项目 TODO + + + + +
+
+

dudu — 项目 TODO

+
生成于 2026-06-12 · 真相源 todo/todo.json
+
+
15全部
+
3待开始
+
0开发中
+
12待验收
+
0已验收
+ +
+
+
+ +
+
+ 重要度 + + + + +
+
+
+ 状态 + + + + + +
+
+
+ 改动等级 + + + + +
+
+
+ 平台 / 标签 + +
+
+ +
+ + +
+
+ 📋 待开始 3 + ▴ 收起 +
+
+
    + +
  • +
    + M0 风险验证 spike +
    + 待开始 + 高优 · 紧急 + 二级 + 2/3 子任务 + +
    +
    + +
    三项风险验证并行:iOS 键盘录音、Gummy 流式、macOS 注入。无前置依赖,第一波启动
    + +
    +
    + 子任务 + 2 / 3 完成 +
    +
    +
    • + + 1A + iOS 键盘扩展内录音可行性 spike(真机验证完全访问下 AVAudioSession;不可行则确认 deep link 降级) +
      二级待验收
      +
    • +
    • + + 1B + Gummy 流式接入 demo(Go 直连 DashScope WS,wav 推流收 partial/final) +
      二级待验收
      +
    • +
    • + + 1C + macOS 注入 + 不抢焦点浮层 demo(剪贴板+CGEvent、focusable:false 验证微信/浏览器/IDE) +
      二级待开始
      +
    +
    + +
  • + +
  • +
    + 微信开放平台与商户号申请 +
    + 待开始 + 高优 · 紧急 + 三级 + 0/3 子任务 + +
    +
    + +
    人工/外部流程,周期数周,是支付联调(#7)与上架的关键路径,第一天启动
    + +
    +
    + 子任务 + 0 / 3 完成 +
    +
    +
    • + + 2A + 开放平台网站应用 + 移动应用注册提审 +
      三级待开始
      +
    • +
    • + + 2B + 微信支付商户号申请 + API 证书/密钥配置(入 Bitwarden) +
      三级待开始
      +
    • +
    • + + 2C + 软件著作权申请(输入法上架需要) +
      三级待开始
      +
    +
    + +
  • + +
  • +
    + 桌面·联调与验收 +
    + 待开始 + 高优 · 紧急 + 二级 + 0/3 子任务 + +
    +
    + +
    汇聚点:依赖 #10 #11 全部与后端 #5 #6 #7
    + +
    +
    + 子任务 + 0 / 3 完成 +
    +
    +
    • + + 12A + 全链路联调:快捷键→采集→流式→浮层→注入(mac+win 双平台) +
      二级待开始
      +
    • +
    • + + 12B + 延迟验收:first_partial P95<500ms / release_to_commit<300ms 实测调优 +
      二级待开始依赖:12A
      +
    • +
    • + + 12C + 异常路径:断网重连/QUOTA/RATE_LIMITED/3 分钟截断逐项验证 +
      二级待开始依赖:12A
      +
    +
    + +
  • +
+
+
+
+
+ 🔨 开发中 0 + ▴ 收起 +
+
+
    +

    暂无条目

    +
+
+
+
+
+ 🔍 待验收 12 + ▴ 收起 +
+
+
    + +
  • +
    + 协议与共享基建 +
    + 待验收 + 高优 · 紧急 + 一级 + 4/4 子任务 + +
    +
    + +
    pkg/protocol 是 5 端对齐的唯一真相源,多数任务前置。设计依据 doc/backend-architecture.html 第四章
    + +
    +
    + 子任务 + 4 / 4 完成 +
    +
    +
    • + + 3A + pkg/protocol:WS 帧结构(start/stop/cancel/partial/final/usage/error)+ 错误码常量 +
      一级待验收
      +
    • +
    • + + 3B + pkg/protocol:REST DTO(me/packs/orders/feedback/metrics) +
      一级待验收
      +
    • +
    • + + 3C + design-pipeline:tokens.css 导出 DuduTheme.swift / DuduTheme.kt + CI 校验 +
      二级待验收
      +
    • +
    • + + 3D + monorepo 骨架(server/desktop/android/ios 目录 + 根 README + CI 雏形) +
      三级待验收
      +
    +
    + +
  • + +
  • +
    + 后端·工程与数据层 +
    + 待验收 + 高优 · 紧急 + 一级 + 3/3 子任务 + +
    +
    + +
    schema 见 doc/backend-architecture.html 第六章
    + +
    +
    + 子任务 + 3 / 3 完成 +
    +
    +
    • + + 4A + Go 工程骨架:gin + 配置加载 + 日志 + Docker compose(pg/redis) +
      二级待验收
      +
    • +
    • + + 4B + store:全部表 migration + gorm models(11 张表,见后端文档第六章) +
      一级待验收依赖:4A
      +
    • +
    • + + 4C + Redis 接入 + Key 封装(quota/authqr/jwt:block/rate 滑动窗口) +
      二级待验收依赖:4A
      +
    +
    + +
  • + +
  • +
    + 后端·认证 +
    + 待验收 + 高优 · 紧急 + 二级 + 3/3 子任务 + +
    +
    + +
    跨 epic 依赖:#4 骨架;5C 需 #2A 微信凭证(开发期可用测试号 mock)
    + +
    +
    + 子任务 + 3 / 3 完成 +
    +
    +
    • + + 5A + JWT 签发/校验中间件 + jti 黑名单 + logout +
      二级待验收
      +
    • +
    • + + 5B + 扫码登录状态机(POST/GET /v1/auth/qr,Redis state TTL 120s) +
      二级待验收依赖:5A
      +
    • +
    • + + 5C + 微信网页授权回调 + 移动端 code 换 token + unionid 关联建号 +
      二级待验收依赖:5A
      +
    +
    + +
  • + +
  • +
    + 后端·识别网关 +
    + 待验收 + 高优 · 紧急 + 一级 + 5/5 子任务 + +
    +
    + +
    跨 epic 依赖:#3A 协议、#4 骨架。含 Provider 抽象/中继/计量/识别限制
    + +
    +
    + 子任务 + 5 / 5 完成 +
    +
    +
    • + + 6A + ASR Provider 接口 + mock provider(脚本化回放 partial/final 供测试) +
      一级待验收
      +
    • +
    • + + 6B + gummy provider(DashScope WS 对接,含断连重建,基于 1B demo) +
      二级待验收依赖:6A
      +
    • +
    • + + 6C + WS 网关:连接鉴权 + 会话状态机 + 上下行双泵中继 +
      一级待验收依赖:6A
      +
    • +
    • + + 6D + 计量与 usage 帧(实收帧时长累计、provider 时间戳交叉校验、2s 下发) +
      二级待验收依赖:6C
      +
    • +
    • + + 6E + 识别限制:3 分钟截断定稿 + 单设备单路 + 30 分钟滑动窗口(次数/时长) +
      二级待验收依赖:6C
      +
    +
    + +
  • + +
  • +
    + 后端·计费与配额 +
    + 待验收 + 高优 · 紧急 + 一级 + 4/4 子任务 + +
    +
    + +
    跨 epic 依赖:#4;7C/7D 真实支付需 #2B 商户号(可先沙箱)。时长账本设计见后端文档第五章
    + +
    +
    + 子任务 + 4 / 4 完成 +
    +
    +
    • + + 7A + quota:Redis Lua 原子扣减(先试用后余额)+ start 预检 + 扣穿当句不掐断 +
      一级待验收
      +
    • +
    • + + 7B + quota:异步落库 ledger/asr_sessions + 余额冗余列 + 每日对账任务 +
      二级待验收依赖:7A
      +
    • +
    • + + 7C + billing:GET /v1/packs + 下单(native code_url / app 支付参数) +
      二级待验收
      +
    • +
    • + + 7D + billing:回调验签幂等入账(transaction_id 唯一)+ 订单轮询 + 5 分钟查单兜底 +
      一级待验收依赖:7C
      +
    +
    + +
  • + +
  • +
    + 桌面·Rust 系统能力 +
    + 待验收 + 高优 · 紧急 + 二级 + 8/8 子任务 + +
    +
    + +
    六项系统能力九成可并行;10C 依赖 #3A 协议
    + +
    +
    + 子任务 + 8 / 8 完成 +
    +
    +
    • + + 10A + Tauri 2 工程 + @dudu/design 接入 + data-theme 主题切换 +
      二级待验收
      +
    • +
    • + + 10B + 全局快捷键 push-to-talk(press/release 事件 + 自定义改键) +
      二级待验收
      +
    • +
    • + + 10C + WS 客户端:预连接/心跳/指数退避重连 + 协议编解码 +
      二级待验收
      +
    • +
    • + + 10D + cpal 音频采集 + 16k 重采样 + 100ms 分帧 + 设备枚举/切换 +
      二级待验收
      +
    • +
    • + + 10E + macOS 文字注入(剪贴板+CGEvent+恢复)+ 辅助功能权限检测(基于 1C demo) +
      二级待验收
      +
    • +
    • + + 10F + Windows 文字注入(SendInput KEYEVENTF_UNICODE) +
      二级待验收
      +
    • +
    • + + 10G + Rust↔Web 事件桥(hotkey/asr/account/audio 事件 + invoke 命令集) +
      二级待验收依赖:10B10C
      +
    • +
    • + + 10H + metrics 模块:本地队列(20 条/60s)+ 批量上报 + 漏斗事件汇入 +
      二级待验收依赖:10C
      +
    +
    + +
  • + +
  • +
    + 桌面·界面 +
    + 待验收 + 高优 · 紧急 + 二级 + 6/6 子任务 + +
    +
    + +
    直接复用 design/ React 组件;可用 mock 事件与 #10 并行,最终联调依赖 10G 事件桥
    + +
    +
    + 子任务 + 6 / 6 完成 +
    +
    +
    • + + 11A + 识别浮层窗口(透明/置顶/不抢焦点 + RecognitionOverlay 两态 + 错误态文案) +
      二级待验收
      +
    • +
    • + + 11B + 托盘 + 自绘托盘菜单(248px、accent 反白、检查更新入口) +
      二级待验收
      +
    • +
    • + + 11C + 设置窗口(四分组:输入/通用/账号与时长/帮助 + 持久化) +
      二级待验收
      +
    • +
    • + + 11D + 登录/购买窗口(扫码轮询 → 时长包 → 支付二维码轮询 → 完成) +
      二级待验收
      +
    • +
    • + + 11E + 反馈窗口(文字 + 图片粘贴/拖拽 + 诊断 Switch + 成功态) +
      二级待验收
      +
    • +
    • + + 11F + 首次启动引导(欢迎 → 权限 → 登录 → 试一试) +
      二级待验收依赖:11C
      +
    +
    + +
  • + +
  • +
    + 后端·用户与周边 +
    + 待验收 + 重要 + 二级 + 4/4 子任务 + +
    +
    + +
    跨 epic 依赖:#4。含 v1.1 新增 feedback/telemetry
    + +
    +
    + 子任务 + 4 / 4 完成 +
    +
    +
    • + + 8A + GET /v1/me 聚合(balance/trial/account_state 判定)+ devices 登记 +
      二级待验收
      +
    • +
    • + + 8B + GET /v1/app/latest 版本检查 +
      三级待验收
      +
    • +
    • + + 8C + feedback:multipart 接收 + magic bytes 校验 + OSS 转存 + 10 条/天限频 +
      二级待验收
      +
    • +
    • + + 8D + telemetry:metrics/batch(白名单/截断/异步入库)+ 每日聚合 P50/P95 + 超标告警 +
      二级待验收
      +
    +
    + +
  • + +
  • +
    + 后端·集成测试 +
    + 待验收 + 重要 + 二级 + 2/2 子任务 + +
    +
    + +
    汇聚点:依赖 #5 #6 #7 #8 主体完成
    + +
    +
    + 子任务 + 2 / 2 完成 +
    +
    +
    • + + 9A + WS E2E:wav 推流断言 partial/final/usage 序列与扣费正确(mock+gummy 双跑) +
      二级待验收
      +
    • +
    • + + 9B + 支付 E2E:沙箱/构造回调测幂等、金额校验、入账 + 限频用例(31 次/超 1800s) +
      二级待验收
      +
    +
    + +
  • + +
  • +
    + Android 输入法 +
    + 待验收 + 重要 + 二级 + 6/6 子任务 + +
    +
    + +
    先于 iOS。UI 依赖 #3C 主题导出;联网依赖 #3A 与后端 #5 #6
    + +
    +
    + 子任务 + 6 / 6 完成 +
    +
    +
    • + + 13A + 工程骨架 + DuduTheme.kt 接入(light/dark) +
      二级待验收
      +
    • +
    • + + 13B + 主 App:微信登录 + 启用引导三步 + 我的页(权限自检/试用进度/反馈入口) +
      二级待验收依赖:13A
      +
    • +
    • + + 13C + 主 App:购买时长页 + 微信 APP 支付 +
      二级待验收依赖:13B
      +
    • +
    • + + 13D + IME Service:键盘 UI 四态(工具条/功能键行/MicBar/partial 文本条) +
      二级待验收依赖:13A
      +
    • +
    • + + 13E + IME:录音 + WS 推流 + commitText 上屏 + 上滑取消 +
      二级待验收依赖:13D
      +
    • +
    • + + 13F + 反馈页 + metrics 队列(WorkManager flush) +
      三级待验收依赖:13B
      +
    +
    + +
  • + +
  • +
    + iOS 输入法 +
    + 待验收 + 重要 + 二级 + 5/5 子任务 + +
    +
    + +
    14D 依赖 #1A spike 结论(主路径或 deep link 降级)。UI 依赖 #3C
    + +
    +
    + 子任务 + 5 / 5 完成 +
    +
    +
    • + + 14A + 工程骨架(App+Keyboard Extension+App Group)+ DuduTheme.swift 接入 +
      二级待验收
      +
    • +
    • + + 14B + 主 App:微信登录 + 引导(含完全访问步)+ 我的 + 购买(APP 支付) +
      二级待验收依赖:14A
      +
    • +
    • + + 14C + 键盘扩展:UI 四态 + textDocumentProxy 上屏框架 +
      二级待验收依赖:14A
      +
    • +
    • + + 14D + 键盘扩展:录音 + WS 推流(按 1A 结论实现主路径或 deep link 降级) +
      二级待验收依赖:14C
      +
    • +
    • + + 14E + 反馈页 + metrics(App Group 队列)+ iPad 布局适配 +
      三级待验收依赖:14B14C
      +
    +
    + +
  • + +
  • +
    + 官网宣传页(web/index.html) +
    + 待验收 + 重要 + 三级 + + +
    +
    + +
    单页静态宣传页,复用设计系统品牌语言,含按住说话交互演示;后续接真实下载链接与备案号
    + + + +
  • +
+
+
+
+
+ ✅ 已验收 0 + ▾ 展开 +
+ +
+ +
+ + + + + + + \ No newline at end of file diff --git a/todo/todo.json b/todo/todo.json new file mode 100644 index 0000000..174a846 --- /dev/null +++ b/todo/todo.json @@ -0,0 +1,793 @@ +{ + "meta": { + "title": "dudu — 项目 TODO", + "updated_at": "2026-06-11T16:34:46.039Z" + }, + "seq": 15, + "items": [ + { + "id": 1, + "title": "M0 风险验证 spike", + "desc": "三项风险验证并行:iOS 键盘录音、Gummy 流式、macOS 注入。无前置依赖,第一波启动", + "level": "high", + "tier": 2, + "tags": [ + "跨端" + ], + "status": "open", + "created_at": "2026-06-11T01:24:09.849Z", + "done": false, + "completed_at": null, + "version": null, + "subtasks": [ + { + "sid": "1A", + "title": "iOS 键盘扩展内录音可行性 spike(真机验证完全访问下 AVAudioSession;不可行则确认 deep link 降级)", + "tier": 2, + "deps": [], + "status": "done", + "created_at": "2026-06-11T01:24:10.547Z" + }, + { + "sid": "1B", + "title": "Gummy 流式接入 demo(Go 直连 DashScope WS,wav 推流收 partial/final)", + "tier": 2, + "deps": [], + "status": "done", + "created_at": "2026-06-11T01:24:10.598Z" + }, + { + "sid": "1C", + "title": "macOS 注入 + 不抢焦点浮层 demo(剪贴板+CGEvent、focusable:false 验证微信/浏览器/IDE)", + "tier": 2, + "deps": [], + "status": "open", + "created_at": "2026-06-11T01:24:10.647Z" + } + ] + }, + { + "id": 2, + "title": "微信开放平台与商户号申请", + "desc": "人工/外部流程,周期数周,是支付联调(#7)与上架的关键路径,第一天启动", + "level": "high", + "tier": 3, + "tags": [ + "外部" + ], + "status": "open", + "created_at": "2026-06-11T01:24:09.900Z", + "done": false, + "completed_at": null, + "version": null, + "subtasks": [ + { + "sid": "2A", + "title": "开放平台网站应用 + 移动应用注册提审", + "tier": 3, + "deps": [], + "status": "open", + "created_at": "2026-06-11T01:24:10.700Z" + }, + { + "sid": "2B", + "title": "微信支付商户号申请 + API 证书/密钥配置(入 Bitwarden)", + "tier": 3, + "deps": [], + "status": "open", + "created_at": "2026-06-11T01:24:10.749Z" + }, + { + "sid": "2C", + "title": "软件著作权申请(输入法上架需要)", + "tier": 3, + "deps": [], + "status": "open", + "created_at": "2026-06-11T01:24:10.798Z" + } + ] + }, + { + "id": 3, + "title": "协议与共享基建", + "desc": "pkg/protocol 是 5 端对齐的唯一真相源,多数任务前置。设计依据 doc/backend-architecture.html 第四章", + "level": "high", + "tier": 1, + "tags": [ + "后端", + "跨端" + ], + "status": "done", + "created_at": "2026-06-11T01:24:09.951Z", + "done": false, + "completed_at": null, + "version": null, + "subtasks": [ + { + "sid": "3A", + "title": "pkg/protocol:WS 帧结构(start/stop/cancel/partial/final/usage/error)+ 错误码常量", + "tier": 1, + "deps": [], + "status": "done", + "created_at": "2026-06-11T01:24:10.847Z" + }, + { + "sid": "3B", + "title": "pkg/protocol:REST DTO(me/packs/orders/feedback/metrics)", + "tier": 1, + "deps": [], + "status": "done", + "created_at": "2026-06-11T01:24:10.898Z" + }, + { + "sid": "3C", + "title": "design-pipeline:tokens.css 导出 DuduTheme.swift / DuduTheme.kt + CI 校验", + "tier": 2, + "deps": [], + "status": "done", + "created_at": "2026-06-11T01:24:10.947Z" + }, + { + "sid": "3D", + "title": "monorepo 骨架(server/desktop/android/ios 目录 + 根 README + CI 雏形)", + "tier": 3, + "deps": [], + "status": "done", + "created_at": "2026-06-11T01:24:10.997Z" + } + ] + }, + { + "id": 4, + "title": "后端·工程与数据层", + "desc": "schema 见 doc/backend-architecture.html 第六章", + "level": "high", + "tier": 1, + "tags": [ + "后端", + "数据库" + ], + "status": "done", + "created_at": "2026-06-11T01:24:10.002Z", + "done": false, + "completed_at": null, + "version": null, + "subtasks": [ + { + "sid": "4A", + "title": "Go 工程骨架:gin + 配置加载 + 日志 + Docker compose(pg/redis)", + "tier": 2, + "deps": [], + "status": "done", + "created_at": "2026-06-11T01:24:11.048Z" + }, + { + "sid": "4B", + "title": "store:全部表 migration + gorm models(11 张表,见后端文档第六章)", + "tier": 1, + "deps": [ + "4A" + ], + "status": "done", + "created_at": "2026-06-11T01:24:11.099Z" + }, + { + "sid": "4C", + "title": "Redis 接入 + Key 封装(quota/authqr/jwt:block/rate 滑动窗口)", + "tier": 2, + "deps": [ + "4A" + ], + "status": "done", + "created_at": "2026-06-11T01:24:11.152Z" + } + ] + }, + { + "id": 5, + "title": "后端·认证", + "desc": "跨 epic 依赖:#4 骨架;5C 需 #2A 微信凭证(开发期可用测试号 mock)", + "level": "high", + "tier": 2, + "tags": [ + "后端" + ], + "status": "done", + "created_at": "2026-06-11T01:24:10.051Z", + "done": false, + "completed_at": null, + "version": null, + "subtasks": [ + { + "sid": "5A", + "title": "JWT 签发/校验中间件 + jti 黑名单 + logout", + "tier": 2, + "deps": [], + "status": "done", + "created_at": "2026-06-11T01:24:11.203Z" + }, + { + "sid": "5B", + "title": "扫码登录状态机(POST/GET /v1/auth/qr,Redis state TTL 120s)", + "tier": 2, + "deps": [ + "5A" + ], + "status": "done", + "created_at": "2026-06-11T01:24:11.254Z" + }, + { + "sid": "5C", + "title": "微信网页授权回调 + 移动端 code 换 token + unionid 关联建号", + "tier": 2, + "deps": [ + "5A" + ], + "status": "done", + "created_at": "2026-06-11T01:24:11.304Z" + } + ] + }, + { + "id": 6, + "title": "后端·识别网关", + "desc": "跨 epic 依赖:#3A 协议、#4 骨架。含 Provider 抽象/中继/计量/识别限制", + "level": "high", + "tier": 1, + "tags": [ + "后端" + ], + "status": "done", + "created_at": "2026-06-11T01:24:10.101Z", + "done": false, + "completed_at": null, + "version": null, + "subtasks": [ + { + "sid": "6A", + "title": "ASR Provider 接口 + mock provider(脚本化回放 partial/final 供测试)", + "tier": 1, + "deps": [], + "status": "done", + "created_at": "2026-06-11T01:24:11.355Z" + }, + { + "sid": "6B", + "title": "gummy provider(DashScope WS 对接,含断连重建,基于 1B demo)", + "tier": 2, + "deps": [ + "6A" + ], + "status": "done", + "created_at": "2026-06-11T01:24:11.405Z" + }, + { + "sid": "6C", + "title": "WS 网关:连接鉴权 + 会话状态机 + 上下行双泵中继", + "tier": 1, + "deps": [ + "6A" + ], + "status": "done", + "created_at": "2026-06-11T01:24:11.454Z" + }, + { + "sid": "6D", + "title": "计量与 usage 帧(实收帧时长累计、provider 时间戳交叉校验、2s 下发)", + "tier": 2, + "deps": [ + "6C" + ], + "status": "done", + "created_at": "2026-06-11T01:24:11.505Z" + }, + { + "sid": "6E", + "title": "识别限制:3 分钟截断定稿 + 单设备单路 + 30 分钟滑动窗口(次数/时长)", + "tier": 2, + "deps": [ + "6C" + ], + "status": "done", + "created_at": "2026-06-11T01:24:11.556Z" + } + ] + }, + { + "id": 7, + "title": "后端·计费与配额", + "desc": "跨 epic 依赖:#4;7C/7D 真实支付需 #2B 商户号(可先沙箱)。时长账本设计见后端文档第五章", + "level": "high", + "tier": 1, + "tags": [ + "后端", + "数据库" + ], + "status": "done", + "created_at": "2026-06-11T01:24:10.150Z", + "done": false, + "completed_at": null, + "version": null, + "subtasks": [ + { + "sid": "7A", + "title": "quota:Redis Lua 原子扣减(先试用后余额)+ start 预检 + 扣穿当句不掐断", + "tier": 1, + "deps": [], + "status": "done", + "created_at": "2026-06-11T01:24:11.606Z" + }, + { + "sid": "7B", + "title": "quota:异步落库 ledger/asr_sessions + 余额冗余列 + 每日对账任务", + "tier": 2, + "deps": [ + "7A" + ], + "status": "done", + "created_at": "2026-06-11T01:24:11.656Z" + }, + { + "sid": "7C", + "title": "billing:GET /v1/packs + 下单(native code_url / app 支付参数)", + "tier": 2, + "deps": [], + "status": "done", + "created_at": "2026-06-11T01:24:11.709Z" + }, + { + "sid": "7D", + "title": "billing:回调验签幂等入账(transaction_id 唯一)+ 订单轮询 + 5 分钟查单兜底", + "tier": 1, + "deps": [ + "7C" + ], + "status": "done", + "created_at": "2026-06-11T01:24:11.760Z" + } + ] + }, + { + "id": 8, + "title": "后端·用户与周边", + "desc": "跨 epic 依赖:#4。含 v1.1 新增 feedback/telemetry", + "level": "mid", + "tier": 2, + "tags": [ + "后端" + ], + "status": "done", + "created_at": "2026-06-11T01:24:10.200Z", + "done": false, + "completed_at": null, + "version": null, + "subtasks": [ + { + "sid": "8A", + "title": "GET /v1/me 聚合(balance/trial/account_state 判定)+ devices 登记", + "tier": 2, + "deps": [], + "status": "done", + "created_at": "2026-06-11T01:24:11.810Z" + }, + { + "sid": "8B", + "title": "GET /v1/app/latest 版本检查", + "tier": 3, + "deps": [], + "status": "done", + "created_at": "2026-06-11T01:24:11.862Z" + }, + { + "sid": "8C", + "title": "feedback:multipart 接收 + magic bytes 校验 + OSS 转存 + 10 条/天限频", + "tier": 2, + "deps": [], + "status": "done", + "created_at": "2026-06-11T01:24:11.913Z" + }, + { + "sid": "8D", + "title": "telemetry:metrics/batch(白名单/截断/异步入库)+ 每日聚合 P50/P95 + 超标告警", + "tier": 2, + "deps": [], + "status": "done", + "created_at": "2026-06-11T01:24:11.964Z" + } + ] + }, + { + "id": 9, + "title": "后端·集成测试", + "desc": "汇聚点:依赖 #5 #6 #7 #8 主体完成", + "level": "mid", + "tier": 2, + "tags": [ + "后端", + "CI/CD" + ], + "status": "done", + "created_at": "2026-06-11T01:24:10.251Z", + "done": false, + "completed_at": null, + "version": null, + "subtasks": [ + { + "sid": "9A", + "title": "WS E2E:wav 推流断言 partial/final/usage 序列与扣费正确(mock+gummy 双跑)", + "tier": 2, + "deps": [], + "status": "done", + "created_at": "2026-06-11T01:24:12.015Z" + }, + { + "sid": "9B", + "title": "支付 E2E:沙箱/构造回调测幂等、金额校验、入账 + 限频用例(31 次/超 1800s)", + "tier": 2, + "deps": [], + "status": "done", + "created_at": "2026-06-11T01:24:12.067Z" + } + ] + }, + { + "id": 10, + "title": "桌面·Rust 系统能力", + "desc": "六项系统能力九成可并行;10C 依赖 #3A 协议", + "level": "high", + "tier": 2, + "tags": [ + "mac", + "Windows" + ], + "status": "done", + "created_at": "2026-06-11T01:24:10.300Z", + "done": false, + "completed_at": null, + "version": null, + "subtasks": [ + { + "sid": "10A", + "title": "Tauri 2 工程 + @dudu/design 接入 + data-theme 主题切换", + "tier": 2, + "deps": [], + "status": "done", + "created_at": "2026-06-11T01:24:12.118Z" + }, + { + "sid": "10B", + "title": "全局快捷键 push-to-talk(press/release 事件 + 自定义改键)", + "tier": 2, + "deps": [], + "status": "done", + "created_at": "2026-06-11T01:24:12.171Z" + }, + { + "sid": "10C", + "title": "WS 客户端:预连接/心跳/指数退避重连 + 协议编解码", + "tier": 2, + "deps": [], + "status": "done", + "created_at": "2026-06-11T01:24:12.222Z" + }, + { + "sid": "10D", + "title": "cpal 音频采集 + 16k 重采样 + 100ms 分帧 + 设备枚举/切换", + "tier": 2, + "deps": [], + "status": "done", + "created_at": "2026-06-11T01:24:12.272Z" + }, + { + "sid": "10E", + "title": "macOS 文字注入(剪贴板+CGEvent+恢复)+ 辅助功能权限检测(基于 1C demo)", + "tier": 2, + "deps": [], + "status": "done", + "created_at": "2026-06-11T01:24:12.321Z" + }, + { + "sid": "10F", + "title": "Windows 文字注入(SendInput KEYEVENTF_UNICODE)", + "tier": 2, + "deps": [], + "status": "done", + "created_at": "2026-06-11T01:24:12.373Z" + }, + { + "sid": "10G", + "title": "Rust↔Web 事件桥(hotkey/asr/account/audio 事件 + invoke 命令集)", + "tier": 2, + "deps": [ + "10B", + "10C" + ], + "status": "done", + "created_at": "2026-06-11T01:24:12.431Z" + }, + { + "sid": "10H", + "title": "metrics 模块:本地队列(20 条/60s)+ 批量上报 + 漏斗事件汇入", + "tier": 2, + "deps": [ + "10C" + ], + "status": "done", + "created_at": "2026-06-11T01:24:12.481Z" + } + ] + }, + { + "id": 11, + "title": "桌面·界面", + "desc": "直接复用 design/ React 组件;可用 mock 事件与 #10 并行,最终联调依赖 10G 事件桥", + "level": "high", + "tier": 2, + "tags": [ + "mac", + "Windows", + "前端" + ], + "status": "done", + "created_at": "2026-06-11T01:24:10.349Z", + "done": false, + "completed_at": null, + "version": null, + "subtasks": [ + { + "sid": "11A", + "title": "识别浮层窗口(透明/置顶/不抢焦点 + RecognitionOverlay 两态 + 错误态文案)", + "tier": 2, + "deps": [], + "status": "done", + "created_at": "2026-06-11T01:24:12.531Z" + }, + { + "sid": "11B", + "title": "托盘 + 自绘托盘菜单(248px、accent 反白、检查更新入口)", + "tier": 2, + "deps": [], + "status": "done", + "created_at": "2026-06-11T01:24:12.582Z" + }, + { + "sid": "11C", + "title": "设置窗口(四分组:输入/通用/账号与时长/帮助 + 持久化)", + "tier": 2, + "deps": [], + "status": "done", + "created_at": "2026-06-11T01:24:12.635Z" + }, + { + "sid": "11D", + "title": "登录/购买窗口(扫码轮询 → 时长包 → 支付二维码轮询 → 完成)", + "tier": 2, + "deps": [], + "status": "done", + "created_at": "2026-06-11T01:24:12.688Z" + }, + { + "sid": "11E", + "title": "反馈窗口(文字 + 图片粘贴/拖拽 + 诊断 Switch + 成功态)", + "tier": 2, + "deps": [], + "status": "done", + "created_at": "2026-06-11T01:24:12.739Z" + }, + { + "sid": "11F", + "title": "首次启动引导(欢迎 → 权限 → 登录 → 试一试)", + "tier": 2, + "deps": [ + "11C" + ], + "status": "done", + "created_at": "2026-06-11T01:24:12.789Z" + } + ] + }, + { + "id": 12, + "title": "桌面·联调与验收", + "desc": "汇聚点:依赖 #10 #11 全部与后端 #5 #6 #7", + "level": "high", + "tier": 2, + "tags": [ + "mac", + "Windows" + ], + "status": "open", + "created_at": "2026-06-11T01:24:10.398Z", + "done": false, + "completed_at": null, + "version": null, + "subtasks": [ + { + "sid": "12A", + "title": "全链路联调:快捷键→采集→流式→浮层→注入(mac+win 双平台)", + "tier": 2, + "deps": [], + "status": "open", + "created_at": "2026-06-11T01:24:12.841Z" + }, + { + "sid": "12B", + "title": "延迟验收:first_partial P95<500ms / release_to_commit<300ms 实测调优", + "tier": 2, + "deps": [ + "12A" + ], + "status": "open", + "created_at": "2026-06-11T01:24:12.893Z" + }, + { + "sid": "12C", + "title": "异常路径:断网重连/QUOTA/RATE_LIMITED/3 分钟截断逐项验证", + "tier": 2, + "deps": [ + "12A" + ], + "status": "open", + "created_at": "2026-06-11T01:24:12.943Z" + } + ] + }, + { + "id": 13, + "title": "Android 输入法", + "desc": "先于 iOS。UI 依赖 #3C 主题导出;联网依赖 #3A 与后端 #5 #6", + "level": "mid", + "tier": 2, + "tags": [ + "Android" + ], + "status": "done", + "created_at": "2026-06-11T01:24:10.447Z", + "done": false, + "completed_at": null, + "version": null, + "subtasks": [ + { + "sid": "13A", + "title": "工程骨架 + DuduTheme.kt 接入(light/dark)", + "tier": 2, + "deps": [], + "status": "done", + "created_at": "2026-06-11T01:24:12.992Z" + }, + { + "sid": "13B", + "title": "主 App:微信登录 + 启用引导三步 + 我的页(权限自检/试用进度/反馈入口)", + "tier": 2, + "deps": [ + "13A" + ], + "status": "done", + "created_at": "2026-06-11T01:24:13.044Z" + }, + { + "sid": "13C", + "title": "主 App:购买时长页 + 微信 APP 支付", + "tier": 2, + "deps": [ + "13B" + ], + "status": "done", + "created_at": "2026-06-11T01:24:13.093Z" + }, + { + "sid": "13D", + "title": "IME Service:键盘 UI 四态(工具条/功能键行/MicBar/partial 文本条)", + "tier": 2, + "deps": [ + "13A" + ], + "status": "done", + "created_at": "2026-06-11T01:24:13.144Z" + }, + { + "sid": "13E", + "title": "IME:录音 + WS 推流 + commitText 上屏 + 上滑取消", + "tier": 2, + "deps": [ + "13D" + ], + "status": "done", + "created_at": "2026-06-11T01:24:13.198Z" + }, + { + "sid": "13F", + "title": "反馈页 + metrics 队列(WorkManager flush)", + "tier": 3, + "deps": [ + "13B" + ], + "status": "done", + "created_at": "2026-06-11T01:24:13.250Z" + } + ] + }, + { + "id": 14, + "title": "iOS 输入法", + "desc": "14D 依赖 #1A spike 结论(主路径或 deep link 降级)。UI 依赖 #3C", + "level": "mid", + "tier": 2, + "tags": [ + "iOS" + ], + "status": "done", + "created_at": "2026-06-11T01:24:10.497Z", + "done": false, + "completed_at": null, + "version": null, + "subtasks": [ + { + "sid": "14A", + "title": "工程骨架(App+Keyboard Extension+App Group)+ DuduTheme.swift 接入", + "tier": 2, + "deps": [], + "status": "done", + "created_at": "2026-06-11T01:24:13.302Z" + }, + { + "sid": "14B", + "title": "主 App:微信登录 + 引导(含完全访问步)+ 我的 + 购买(APP 支付)", + "tier": 2, + "deps": [ + "14A" + ], + "status": "done", + "created_at": "2026-06-11T01:24:13.354Z" + }, + { + "sid": "14C", + "title": "键盘扩展:UI 四态 + textDocumentProxy 上屏框架", + "tier": 2, + "deps": [ + "14A" + ], + "status": "done", + "created_at": "2026-06-11T01:24:13.406Z" + }, + { + "sid": "14D", + "title": "键盘扩展:录音 + WS 推流(按 1A 结论实现主路径或 deep link 降级)", + "tier": 2, + "deps": [ + "14C" + ], + "status": "done", + "created_at": "2026-06-11T01:24:13.461Z" + }, + { + "sid": "14E", + "title": "反馈页 + metrics(App Group 队列)+ iPad 布局适配", + "tier": 3, + "deps": [ + "14B", + "14C" + ], + "status": "done", + "created_at": "2026-06-11T01:24:13.516Z" + } + ] + }, + { + "id": 15, + "title": "官网宣传页(web/index.html)", + "desc": "单页静态宣传页,复用设计系统品牌语言,含按住说话交互演示;后续接真实下载链接与备案号", + "level": "mid", + "tier": 3, + "tags": [ + "Web", + "前端" + ], + "status": "done", + "created_at": "2026-06-11T13:40:06.825Z", + "done": false, + "completed_at": null, + "version": null + } + ] +} diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..677eff4 --- /dev/null +++ b/web/index.html @@ -0,0 +1,623 @@ + + + + + +dudu — 按住说话,文字上屏 + + + + + + + + + + + + +
+
+
+

按住说话
文字上屏

+
+ 大模型识别· + 流式实时· + 全平台输入法 +
+
+ + + 免费下载 + + 每天免费 3 分钟 · 微信登录即用 +
+
+ + +
+
+
备忘录 — 周报草稿
+
+ 光标在这里 — 在任意应用按住快捷键即可语音输入 +
+
+ +
+ + 聆听中… +
+ +
松开 ⌘⇧Space 完成输入
+
+
+ +
就像在你电脑上按住 Space 一样
+
+
+
+ + +
+
+
+
<500ms
+
按下到首字出现
+
+
+
5
+
Mac · Windows · iPhone · iPad · Android
+
+
+
3分钟/天
+
免费试用 · 不需要先付费
+
+
+
+ + +
+
+
+
为什么是 dudu
+

识别得准,上屏得快
用起来像没有这层软件

+
+
+
+ +

大模型识别

+

云端大模型听写,中英混说、专有名词、自动标点都不在话下,说完即定稿

+
+
+ +

流式实时上屏

+

说话的同时文字就出现,不等你说完;松开按键,整句注入焦点输入框

+
+
+ +

哪里都能用

+

桌面端快捷键全局可用,手机上是真正的系统键盘——微信、文档、代码编辑器,焦点在哪写到哪

+
+
+
+
+ + +
+
+
+
怎么用
+

三个动作,零学习成本

+
+
+
+
01
+

按住 Space

+

桌面端任意输入框旁按住快捷键;手机上按住键盘的大麦克风

+
+
+
02
+

说话

+

识别结果实时浮现,说错了上滑取消,不打断你的思路

+
+
+
03
+

松开,上屏

+

整句文字直接写进光标处,标点已排好,接着说下一句就行

+
+
+
+
+ + +
+
+
+
全平台
+

你的每一台设备

+
+
+ Mac + Windows + iPhone键盘扩展 + iPad + Android系统输入法 +
+
+
+ + +
+
+
+
价格
+

按时长买,永不过期
不订阅、不扣月费

+
+
+
+
100 分钟
+
¥9
+
约 ¥0.09 / 分钟
+ 选这档 +
+
+ 最多人选 · 省 13% +
500 分钟
+
¥39
+
约 ¥0.078 / 分钟
+ 选这档 +
+
+ 省 28% +
2000 分钟
+
¥129
+
约 ¥0.065 / 分钟
+ 选这档 +
+
+
每天另有 3 分钟免费试用 · 微信支付,到账即用 · 时长永不过期
+
+
+ + +
+
+ +

从下一句话开始
用说的

+

下载 dudu,微信扫码登录,每天免费 3 分钟

+ 免费下载 dudu +
+
+ + + + + + +