├── .commitlintrc.json ├── .gitattributes ├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .vscode └── extensions.json ├── LICENSE.txt ├── NOTICE ├── PRIVACY.md ├── README.md ├── asset ├── picture-cn.md ├── picture-en.md ├── picture.md ├── select_language.png ├── 主界面.png ├── 其他搜索.png ├── 坚果云.png ├── 外观设置.png ├── 所有程序.png ├── 拼音匹配.png ├── 拼音模糊匹配.png ├── 模糊匹配.png ├── 程序搜索.png ├── 精准匹配.png ├── 自定义图片位置.png ├── 自定义背景.png ├── 自我介绍.png ├── 设置界面.png ├── 调试.png └── 远程管理.png ├── bun.lock ├── doc ├── Feature_Implementation_Guide_cn.md ├── Feature_Implementation_Guide_cn2.md ├── Feature_Implementation_Guide_en.md └── 变量命名规范.md ├── index.html ├── manifests └── g │ └── ghost-him │ └── ZeroLaunch-rs │ └── 0.4.14 │ ├── ghost-him.ZeroLaunch-rs.installer.yaml │ ├── ghost-him.ZeroLaunch-rs.locale.en-US.yaml │ └── ghost-him.ZeroLaunch-rs.yaml ├── package.json ├── public ├── tauri.svg └── vite.svg ├── readme-cn2.md ├── readme-en.md ├── src-tauri ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── build.rs ├── capabilities │ ├── default.json │ └── desktop.json ├── icons │ ├── 128x128.png │ ├── 128x128@2x.png │ ├── 32x32-white.png │ ├── 32x32.png │ ├── Square107x107Logo.png │ ├── Square142x142Logo.png │ ├── Square150x150Logo.png │ ├── Square284x284Logo.png │ ├── Square30x30Logo.png │ ├── Square310x310Logo.png │ ├── Square44x44Logo.png │ ├── Square71x71Logo.png │ ├── Square89x89Logo.png │ ├── StoreLogo.png │ ├── icon.icns │ ├── icon.ico │ ├── icon.png │ ├── terminal.png │ ├── tips.png │ └── web_pages.png ├── models │ └── readme.md ├── src │ ├── commands │ │ ├── config_file.rs │ │ ├── debug.rs │ │ ├── mod.rs │ │ ├── program_service.rs │ │ ├── shortcut.rs │ │ ├── ui_command.rs │ │ └── utils.rs │ ├── core │ │ ├── ai │ │ │ ├── ai_loader.rs │ │ │ ├── embedding_model │ │ │ │ ├── embedding_gemma.rs │ │ │ │ └── mod.rs │ │ │ ├── mod.rs │ │ │ ├── model_manager.rs │ │ │ └── text_generation_model.rs │ │ ├── image_processor.rs │ │ ├── mod.rs │ │ └── storage │ │ │ ├── config.rs │ │ │ ├── local_save.rs │ │ │ ├── mod.rs │ │ │ ├── onedrive.rs │ │ │ ├── storage_manager.rs │ │ │ ├── utils.rs │ │ │ ├── webdav.rs │ │ │ └── windows_utils.rs │ ├── error.rs │ ├── lib.rs │ ├── logging.rs │ ├── main.rs │ ├── modules │ │ ├── config │ │ │ ├── app_config.rs │ │ │ ├── config_manager.rs │ │ │ ├── default.rs │ │ │ ├── mod.rs │ │ │ ├── ui_config.rs │ │ │ └── window_state.rs │ │ ├── mod.rs │ │ ├── program_manager │ │ │ ├── config │ │ │ │ ├── image_loader_config.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── program_loader_config.rs │ │ │ │ ├── program_manager_config.rs │ │ │ │ └── program_ranker_config.rs │ │ │ ├── image_loader.rs │ │ │ ├── localization_translation.rs │ │ │ ├── mod.rs │ │ │ ├── pinyin.json │ │ │ ├── pinyin_mapper.rs │ │ │ ├── program_launcher.rs │ │ │ ├── program_loader.rs │ │ │ ├── program_ranker.rs │ │ │ ├── search_engine.rs │ │ │ ├── search_model │ │ │ │ ├── ai_fuzzy_search_model.rs │ │ │ │ ├── launchy_search_model.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── skim_search_model.rs │ │ │ │ └── standard_search_model.rs │ │ │ ├── semantic_backend.rs │ │ │ ├── semantic_manager.rs │ │ │ ├── unit.rs │ │ │ └── window_activator.rs │ │ ├── shortcut_manager │ │ │ ├── mod.rs │ │ │ └── shortcut_config.rs │ │ ├── ui_controller │ │ │ ├── controller.rs │ │ │ └── mod.rs │ │ └── version_checker │ │ │ └── mod.rs │ ├── state │ │ ├── app_state.rs │ │ └── mod.rs │ ├── tray.rs │ ├── utils │ │ ├── access_policy.rs │ │ ├── defer.rs │ │ ├── font_database.rs │ │ ├── i18n.rs │ │ ├── locale.rs │ │ ├── mod.rs │ │ ├── notify.rs │ │ ├── service_locator.rs │ │ ├── ui_controller.rs │ │ ├── waiting_hashmap.rs │ │ └── windows.rs │ ├── window_effect.rs │ └── window_position.rs ├── tauri.conf.json └── tauri.conf.portable.json ├── src ├── api │ ├── local_config_types.ts │ └── remote_config_types.ts ├── assets │ └── icon.svg ├── i18n │ ├── index.ts │ └── locales │ │ ├── en.json │ │ ├── zh-Hans.json │ │ └── zh-Hant.json ├── main.ts ├── router.ts ├── stores │ ├── local_config.ts │ └── remote_config.ts ├── utils │ ├── ShortcutInput.vue │ ├── SubMenu.vue │ └── color.ts ├── views │ ├── App.vue │ ├── AppConfigSetting.vue │ ├── ConfigPathSelector.vue │ ├── ProgramIndex.vue │ ├── Router.vue │ ├── SettingWindow.vue │ ├── ShortcutSetting.vue │ ├── UIConfigSetting.vue │ ├── about.vue │ ├── components │ │ ├── AnimatedInput.vue │ │ ├── BackgroundImageSettings.vue │ │ ├── SearchResultSettings.vue │ │ └── WindowSettings.vue │ ├── debug.vue │ └── welcome.vue └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json ├── utils ├── convert_svg_to_icon.py └── icon.svg ├── vite.config.ts └── xtask ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── README.md └── src └── main.rs /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@commitlint/config-conventional"], 3 | "rules": { 4 | "type-enum": [ 5 | 2, 6 | "always", 7 | [ 8 | "feat", 9 | "fix", 10 | "docs", 11 | "style", 12 | "refactor", 13 | "perf", 14 | "test", 15 | "build", 16 | "ci", 17 | "chore", 18 | "revert" 19 | ] 20 | ], 21 | "type-case": [2, "always", "lower-case"], 22 | "type-empty": [2, "never"], 23 | "scope-case": [2, "always", [ 24 | "lower-case", 25 | "kebab-case", 26 | "camel-case", 27 | "pascal-case" 28 | ]], 29 | "subject-empty": [2, "never"], 30 | "subject-full-stop": [2, "never", "."], 31 | "header-max-length": [2, "always", 100], 32 | "body-leading-blank": [1, "always"], 33 | "footer-leading-blank": [1, "always"] 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - dev 8 | pull_request: 9 | branches: 10 | - main 11 | workflow_dispatch: 12 | 13 | env: 14 | CARGO_TERM_COLOR: always 15 | 16 | jobs: 17 | test: 18 | name: Test on Windows 19 | runs-on: windows-latest 20 | 21 | steps: 22 | - name: Checkout repository 23 | uses: actions/checkout@v4 24 | 25 | - name: Setup Rust 26 | uses: dtolnay/rust-toolchain@stable 27 | with: 28 | toolchain: stable 29 | components: rustfmt, clippy 30 | 31 | - name: Setup Bun 32 | uses: oven-sh/setup-bun@v2 33 | with: 34 | bun-version: latest 35 | 36 | - name: Install dependencies 37 | run: bun install 38 | 39 | - name: Copy i18n locales to src-tauri 40 | shell: pwsh 41 | run: | 42 | $srcLocalesDir = "src\i18n\locales" 43 | $destDir = "src-tauri\locales" 44 | if (-not (Test-Path $destDir)) { 45 | New-Item -Path $destDir -ItemType Directory -Force | Out-Null 46 | } 47 | Copy-Item -Path "$srcLocalesDir\*" -Destination $destDir -Force 48 | Write-Host "✓ i18n locales files copied to src-tauri/locales/" 49 | 50 | - name: Build frontend assets 51 | run: bun run build 52 | 53 | - name: Rust cache 54 | uses: Swatinem/rust-cache@v2 55 | with: 56 | workspaces: src-tauri -> target 57 | 58 | - name: Validate commit messages 59 | if: github.event_name == 'pull_request' 60 | run: | 61 | git fetch origin ${{ github.base_ref }} 62 | bun x commitlint --from origin/${{ github.base_ref }} --to HEAD --verbose 63 | 64 | - name: Check formatting 65 | working-directory: src-tauri 66 | run: cargo fmt --all -- --check 67 | 68 | - name: Clippy 69 | working-directory: src-tauri 70 | run: cargo clippy --all-targets --all-features -- -D warnings 71 | 72 | - name: Run tests 73 | working-directory: src-tauri 74 | run: cargo test --all-features 75 | 76 | build-test: 77 | name: Build Test (x64) 78 | runs-on: windows-latest 79 | needs: test 80 | 81 | steps: 82 | - name: Checkout repository 83 | uses: actions/checkout@v4 84 | 85 | - name: Setup Rust 86 | uses: dtolnay/rust-toolchain@stable 87 | with: 88 | toolchain: stable 89 | targets: x86_64-pc-windows-msvc 90 | 91 | - name: Setup Bun 92 | uses: oven-sh/setup-bun@v2 93 | with: 94 | bun-version: latest 95 | 96 | - name: Install dependencies 97 | run: bun install 98 | 99 | - name: Copy i18n locales to src-tauri 100 | shell: pwsh 101 | run: | 102 | $srcLocalesDir = "src\i18n\locales" 103 | $destDir = "src-tauri\locales" 104 | if (-not (Test-Path $destDir)) { 105 | New-Item -Path $destDir -ItemType Directory -Force | Out-Null 106 | } 107 | Copy-Item -Path "$srcLocalesDir\*" -Destination $destDir -Force 108 | Write-Host "✓ i18n locales files copied to src-tauri/locales/" 109 | 110 | - name: Build frontend assets 111 | run: bun run build 112 | 113 | - name: Rust cache 114 | uses: Swatinem/rust-cache@v2 115 | with: 116 | workspaces: src-tauri -> target 117 | 118 | - name: Build x64 installer (AI enabled) 119 | shell: pwsh 120 | run: | 121 | cd xtask 122 | cargo run --bin xtask build-installer --arch x64 --ai enabled 123 | 124 | - name: Upload artifacts 125 | uses: actions/upload-artifact@v4 126 | with: 127 | name: build-test-artifacts 128 | path: | 129 | *.exe 130 | *.msi 131 | retention-days: 7 132 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | # 以下文件为程序运行过程中的配置文件,对项目本身无作用 27 | config.json 28 | logs 29 | 30 | package-lock.json 31 | *.msi 32 | *.exe 33 | *.zip 34 | 35 | target -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | bun x --no -- commitlint --edit $1 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | set -e 2 | 3 | echo "Running Rust checks (fmt, clippy, test) in src-tauri..." 4 | 5 | # 格式检查(不自动修改,确保 CI 一致) 6 | ( cd src-tauri && cargo fmt --all -- --check ) 7 | 8 | # 静态检查(作为错误处理) 9 | ( cd src-tauri && cargo clippy --all-targets --all-features -- -D warnings ) 10 | 11 | # 单元测试 12 | ( cd src-tauri && cargo test --all-features ) 13 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "Vue.volar", 4 | "tauri-apps.tauri-vscode", 5 | "rust-lang.rust-analyzer" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Gemma is provided under and subject to the Gemma Terms of Use found at https://ai.google.dev/gemma/terms 2 | 3 | This distribution may include unmodified model files from Google's EmbeddingGemma (see src-tauri/EmbeddingGemma-300m/). If any files were modified, they carry notices within the repository history indicating such modifications. 4 | 5 | Use of Gemma is subject to additional restrictions described in the Gemma Prohibited Use Policy: https://ai.google.dev/gemma/prohibited_use_policy 6 | 7 | “Google” and “Gemma” are trademarks of Google LLC. This project is not affiliated with, endorsed by, or sponsored by Google. 8 | -------------------------------------------------------------------------------- /PRIVACY.md: -------------------------------------------------------------------------------- 1 | ### ZeroLaunch-rs 隐私政策 2 | 3 | **最后更新日期:** 2025年7月11日 4 | 5 | 欢迎您使用由 **ghost-him**(以下简称“我们”)开发的 **ZeroLaunch-rs**! 6 | 7 | 我们深知隐私对您的重要性,并致力于在 ZeroLaunch-rs 的设计、开发和运营中保护您的个人信息安全。本软件的核心设计理念是“本地优先”和“用户控制”,我们不会收集您的任何个人数据。 8 | 9 | 请您在使用 ZeroLaunch-rs 前,仔细阅读并充分理解本《隐私政策》。 10 | 11 | #### 一句话总结 12 | 13 | **ZeroLaunch-rs 不会收集、上传或分享您的任何个人数据。所有数据都存储在您的本地设备上。如果您选择使用 WebDAV 功能,您的配置文件将同步到您自己指定的服务器,我们无法访问。** 14 | 15 | --- 16 | 17 | #### 1. 我们不收集任何信息 18 | 19 | ZeroLaunch-rs 是一款纯粹的本地应用程序。我们郑重承诺: 20 | * **无用户追踪:** 我们不会收集任何关于您的使用行为、操作习惯或个人身份的信息。 21 | * **无数据上传:** 软件在默认情况下不会将其在您设备上产生的任何数据(包括配置、日志等)发送给我们或任何第三方。 22 | * **无第三方SDK:** 软件不包含任何用于统计分析、广告或社交分享的第三方软件开发工具包(SDK)。 23 | 24 | #### 2. 数据存储 25 | 26 | ZeroLaunch-rs 产生的所有数据,包括您的设置和配置文件,都完整地存储在您的本地计算机硬盘上。这些数据完全在您的控制之下,除非您手动将其转移,否则它们不会离开您的设备。 27 | 28 | #### 3. 可选的配置文件云同步功能 (WebDAV) 29 | 30 | ZeroLaunch-rs 为您提供了一个完全可选的功能,允许您通过 WebDAV 协议将您的配置文件同步到云端,以便在多台设备间保持一致。 31 | 32 | 关于此功能,请您知悉: 33 | 34 | * **用户自主控制:** 此功能默认关闭。只有在您**主动开启**并**手动填写**您自己的 WebDAV 服务器地址、账户和密码后,同步才会进行。 35 | * **我们无法访问:** ZeroLaunch-rs 仅作为执行同步指令的客户端工具。我们无法、也无权访问您所配置的任何 WebDAV 服务器或其中的数据。 36 | * **您的责任(服务商选择):** 您需要对您选择的 WebDAV 服务的可靠性、安全性和隐私政策负责。我们强烈建议您在使用前,了解并信任您所选服务的相关条款。 37 | * **您的责任(设备安全)**:请您务必妥善保管您的计算机设备。任何因您个人设备丢失、被盗、感染病毒或遭受黑客攻击等原因,导致存储在本地的 WebDAV 配置信息(如服务器地址、账户、密码等)被泄露,所造成的任何数据安全风险和损失,均由您自行承担,与本软件及开发者无关。 38 | 39 | #### 4. 隐私政策的变更 40 | 41 | 我们可能会适时更新本隐私政策。当政策发生变更时,我们会在项目主页或软件发布页面发布更新后的版本。我们建议您定期查阅,以了解最新的隐私政策。 42 | 43 | #### 5. 联系我们 44 | 45 | 如果您对本隐私政策有任何疑问、意见或建议,欢迎通过以下方式与开发者 **ghost-him** 联系: 46 | 47 | * **GitHub Issues 页面:** https://github.com/ghost-him/ZeroLaunch-rs/issues 48 | * **Gitee Issues 页面:** https://gitee.com/ghost-him/ZeroLaunch-rs/issues 49 | * **Gitcode Issues 页面** https://gitcode.com/ghost-him/ZeroLaunch-rs/issues 50 | 51 | 感谢您的信任与使用! -------------------------------------------------------------------------------- /asset/picture-cn.md: -------------------------------------------------------------------------------- 1 | 主介面 2 | ![主介面](主界面.png) 3 | 4 | 精準匹配 5 | ![精準匹配](精准匹配.png) 6 | 7 | 模糊匹配 8 | ![模糊匹配](模糊匹配.png) 9 | 10 | 拼音匹配 11 | ![拼音匹配](拼音匹配.png) 12 | 13 | **拼音匹配也支援模糊匹配** 14 | 假設要輸入 `wy` 時,手指同時按下了 `y` 與 `u`,打出了 `wyu` 的情況 15 | ![拼音模糊匹配](拼音模糊匹配.png) 16 | 17 | 自訂背景 18 | ![自訂背景](自定义背景.png) 19 | 20 | 圖片來源:[@satori_aiart](https://x.com/satori_aiart/status/1728977252946473051) 21 | 22 | 圖片支援自訂位置與大小 23 | 24 | ![自訂背景2](自定义图片位置.png) 25 | 26 | 圖片來源:[@shalldie](https://github.com/shalldie/vscode-background/issues/106) 27 | 28 | 設定介面 29 | ![設定介面](设置界面.png) 30 | 31 | 外觀設定 32 | ![外觀設定](外观设置.png) 33 | 34 | 程式搜尋 35 | ![程式搜尋](程序搜索.png) 36 | 37 | 其他搜尋(可添加檔案、網頁、指令搜尋) 38 | ![其他搜尋](其他搜索.png) 39 | 40 | 遠端管理(用於變更遠端配置檔的儲存位置) 41 | ![遠端管理](远程管理.png) 42 | 43 | 所有程式 44 | ![所有程式](所有程序.png) 45 | 46 | 關於 47 | ![關於](自我介绍.png) 48 | 49 | 偵錯頁面(需手動開啟偵錯模式) 50 | ![偵錯](调试.png) -------------------------------------------------------------------------------- /asset/picture-en.md: -------------------------------------------------------------------------------- 1 | Main Interface 2 | ![Main Interface](主界面.png) 3 | 4 | Exact Match 5 | ![Exact Match](精准匹配.png) 6 | 7 | Fuzzy Match 8 | ![Fuzzy Match](模糊匹配.png) 9 | 10 | Pinyin Match 11 | ![Pinyin Match](拼音匹配.png) 12 | 13 | **Pinyin Match also supports fuzzy matching** 14 | Example: When intending to input `wy`, fingers accidentally press `y` and `u` simultaneously, resulting in `wyu` 15 | ![Pinyin Fuzzy Match](拼音模糊匹配.png) 16 | 17 | Custom Background 18 | ![Custom Background](自定义背景.png) 19 | 20 | Image Source: [@satori_aiart](https://x.com/satori_aiart/status/1728977252946473051) 21 | 22 | Images support custom positioning and sizing 23 | 24 | ![Custom Background 2](自定义图片位置.png) 25 | 26 | Image source: [@shalldie](https://github.com/shalldie/vscode-background/issues/106) 27 | 28 | Settings Interface 29 | ![Settings Interface](设置界面.png) 30 | 31 | Appearance Settings 32 | ![Appearance Settings](外观设置.png) 33 | 34 | Program Search 35 | ![Program Search](程序搜索.png) 36 | 37 | Other Searches (Supports adding files, webpages, commands) 38 | ![Other Searches](其他搜索.png) 39 | 40 | Remote Management (For changing the save location of remote configuration files) 41 | ![Remote Management](远程管理.png) 42 | 43 | All Programs 44 | ![All Programs](所有程序.png) 45 | 46 | About 47 | ![About](自我介绍.png) 48 | 49 | Debug Page (Requires manually enabling debug mode) 50 | ![Debug](调试.png) 51 | -------------------------------------------------------------------------------- /asset/picture.md: -------------------------------------------------------------------------------- 1 | 主界面 2 | 3 | ![主界面](主界面.png) 4 | 5 | 精准匹配 6 | 7 | ![精准匹配](精准匹配.png) 8 | 9 | 模糊匹配 10 | 11 | ![模糊匹配](模糊匹配.png) 12 | 13 | 拼音匹配 14 | 15 | ![拼音匹配](拼音匹配.png) 16 | 17 | **拼音匹配也支持模糊匹配** 18 | 19 | 假设要输入`wy`时,手指同时按下了`y`与`u`,打出来了`wyu`的情况 20 | 21 | ![拼音模糊匹配](拼音模糊匹配.png) 22 | 23 | 自定义背景 24 | 25 | ![自定义背景](自定义背景.png) 26 | 27 | 图片来源:[@satori_aiart](https://x.com/satori_aiart/status/1728977252946473051) 28 | 29 | 图片支持自定义位置与大小 30 | 31 | ![自定义背景2](自定义图片位置.png) 32 | 33 | 图片来源:[@shalldie](https://github.com/shalldie/vscode-background/issues/106) 34 | 35 | 设置界面 36 | 37 | ![设置界面](设置界面.png) 38 | 39 | 外观设置 40 | 41 | ![外观设置](外观设置.png) 42 | 43 | 程序搜索 44 | 45 | ![程序搜索](程序搜索.png) 46 | 47 | 其他搜索(可添加文件,网页,命令搜索) 48 | 49 | ![其他搜索](其他搜索.png) 50 | 51 | 远程管理(用于更改远程配置文件的保存地址) 52 | 53 | ![远程管理](远程管理.png) 54 | 55 | 所有程序 56 | 57 | ![所有程序](所有程序.png) 58 | 59 | 关于 60 | 61 | ![关于](自我介绍.png) 62 | 63 | 调试页面(需要手动开启调试模式) 64 | 65 | ![调试](调试.png) -------------------------------------------------------------------------------- /asset/select_language.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghost-him/ZeroLaunch-rs/414cb78f11ff003b0b8e9e6355c30abd767d70cc/asset/select_language.png -------------------------------------------------------------------------------- /asset/主界面.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghost-him/ZeroLaunch-rs/414cb78f11ff003b0b8e9e6355c30abd767d70cc/asset/主界面.png -------------------------------------------------------------------------------- /asset/其他搜索.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghost-him/ZeroLaunch-rs/414cb78f11ff003b0b8e9e6355c30abd767d70cc/asset/其他搜索.png -------------------------------------------------------------------------------- /asset/坚果云.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghost-him/ZeroLaunch-rs/414cb78f11ff003b0b8e9e6355c30abd767d70cc/asset/坚果云.png -------------------------------------------------------------------------------- /asset/外观设置.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghost-him/ZeroLaunch-rs/414cb78f11ff003b0b8e9e6355c30abd767d70cc/asset/外观设置.png -------------------------------------------------------------------------------- /asset/所有程序.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghost-him/ZeroLaunch-rs/414cb78f11ff003b0b8e9e6355c30abd767d70cc/asset/所有程序.png -------------------------------------------------------------------------------- /asset/拼音匹配.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghost-him/ZeroLaunch-rs/414cb78f11ff003b0b8e9e6355c30abd767d70cc/asset/拼音匹配.png -------------------------------------------------------------------------------- /asset/拼音模糊匹配.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghost-him/ZeroLaunch-rs/414cb78f11ff003b0b8e9e6355c30abd767d70cc/asset/拼音模糊匹配.png -------------------------------------------------------------------------------- /asset/模糊匹配.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghost-him/ZeroLaunch-rs/414cb78f11ff003b0b8e9e6355c30abd767d70cc/asset/模糊匹配.png -------------------------------------------------------------------------------- /asset/程序搜索.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghost-him/ZeroLaunch-rs/414cb78f11ff003b0b8e9e6355c30abd767d70cc/asset/程序搜索.png -------------------------------------------------------------------------------- /asset/精准匹配.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghost-him/ZeroLaunch-rs/414cb78f11ff003b0b8e9e6355c30abd767d70cc/asset/精准匹配.png -------------------------------------------------------------------------------- /asset/自定义图片位置.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghost-him/ZeroLaunch-rs/414cb78f11ff003b0b8e9e6355c30abd767d70cc/asset/自定义图片位置.png -------------------------------------------------------------------------------- /asset/自定义背景.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghost-him/ZeroLaunch-rs/414cb78f11ff003b0b8e9e6355c30abd767d70cc/asset/自定义背景.png -------------------------------------------------------------------------------- /asset/自我介绍.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghost-him/ZeroLaunch-rs/414cb78f11ff003b0b8e9e6355c30abd767d70cc/asset/自我介绍.png -------------------------------------------------------------------------------- /asset/设置界面.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghost-him/ZeroLaunch-rs/414cb78f11ff003b0b8e9e6355c30abd767d70cc/asset/设置界面.png -------------------------------------------------------------------------------- /asset/调试.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghost-him/ZeroLaunch-rs/414cb78f11ff003b0b8e9e6355c30abd767d70cc/asset/调试.png -------------------------------------------------------------------------------- /asset/远程管理.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghost-him/ZeroLaunch-rs/414cb78f11ff003b0b8e9e6355c30abd767d70cc/asset/远程管理.png -------------------------------------------------------------------------------- /doc/Feature_Implementation_Guide_cn.md: -------------------------------------------------------------------------------- 1 | # 常见功能的实现方法 2 | 3 | ## 有一些不会被使用的程序被添加了 4 | 5 | 常见的,不会被使用的程序有: 6 | * 各类程序的卸载程序 7 | * 各类程序的帮助文档 8 | 9 | 这程序已经默认添加到屏蔽关键字中,而如果有一些没有被包括的程序也被添加了,可以通过以下的方式完成程序的屏蔽: 10 | 1. 打开设置界面 11 | 2. 点击“程序搜索” 12 | 3. 点击“设置屏蔽关键字” 13 | 4. 点击“添加项目” 14 | 5. 输入要屏蔽的程序 15 | 6. 点击“保存配置文件”,程序会自动保存当前的配置并重新加载配置 16 | 17 | 只要程序中出现了关键字,则直接屏蔽,所以写入 `help` 时,可以直接将所有的 `xxx help` 程序全部屏蔽 18 | 19 | ## 更改了安装路径的程序没有被检测到 20 | 21 | 程序会遍历默认安装路径下的所有的应用,如果有自定义安装的路径没有被检测到,可以通过以下的方式添加 22 | 1. 打开设置界面 23 | 2. 点击“程序搜索” 24 | 3. 点击“设置遍历路径” 25 | 4. 点击“添加项目” 26 | 5. 将安装目录添加并设置对应的遍历深度 27 | 6. 点击“保存配置文件”,程序会自动保存当前的配置并重新加载配置 28 | 29 | 可能的问题:[什么是遍历深度](#什么是遍历深度) 30 | 31 | ## 添加网址/命令(可自定义打开windows设置与各类控制台) 32 | 33 | 1. 打开设置界面 34 | 2. 点击“其他搜索” 35 | 3. 点击对应的标签页 36 | 4. 完成添加 37 | 5. 点击“保存配置文件”,程序会自动保存当前的配置并重新加载配置 38 | 39 | 可能的问题:[什么是关键字](#什么是关键字) 40 | 41 | 添加windows设置的方法:使用命令:`explorer.exe ms-settings:[目标]`,可以在网上查到`ms-settings`支持的设置。以显示设置为例:`explorer.exe ms-settings:display`。 42 | 43 | 添加各类控制台:使用` Get-ChildItem -Path C:\Windows\system32\* -Include *.msc | Sort-Object -Property Extension | Select-Object -Property Name | Format-Wide -Column 1`可看到支持的控制台。使用命令:`mmc [目标控制台]`。以本地策略编辑器 `gpedit.msc` 为例:使用命令:`mmc gpedit.msc` 44 | 45 | ## 对搜索算法的微调怎么做 46 | 47 | 首先要对这个搜索算法的处理流程做一定的了解。推荐配合着代码看,对应的代码实现在`src-tauri/src/modules/program_manager/mod.rs`,更新搜索算法的函数为`update`。 48 | 49 | 这个搜索算法的核心思路是,对于用户的输入,每一个程序都有一个“匹配值”,而这个匹配值表示用户预期目标为当前程序的可能性。匹配值越大,则表示用户的目标程序越有可能是当前的程序。所以程序的结果栏显示的也是所有程序中匹配值最大的几个。 50 | 51 | 一个程序的匹配值由以下几个部分组成:字符串匹配值 + 固定权重 + 动态权重。 52 | * 字符串匹配值:由用户输入的字符串与搜索关键字计算而来(固定变化)。 53 | * 固定权重:用户设置的目标程序的固定权重(用户决定)。 54 | * 动态权重:根据历史启动次数计算而来(动态变化)。 55 | 56 | 而用户可以更改**固定权重**的值。注意,固定权重的赋值与**屏蔽关键字**一样。 57 | 58 | 更改其值的方式如下所示: 59 | 1. 打开设置界面 60 | 2. 点击“程序搜索” 61 | 3. 点击“设置固定偏移量” 62 | 4. 点击“添加项目” 63 | 5. 设置对应的值 64 | 6. 点击“保存配置文件” 65 | 66 | ## 配置文件保存地址的更换 67 | 68 | ### 更换本地保存路径 69 | 70 | 1. 打开设置界面 71 | 2. 点击“远程管理” 72 | 3. 点击“本地存储” 73 | 4. 点击“选择路径”按钮 74 | 5. 选择你要保存的文件夹,选择成功后则自动保存到目标路径 75 | 6. 点击“测试连接” 76 | 7. 点击“保存配置” 77 | 78 | 程序在测试时,会在目标文件夹下创建一个测试文件,可手动删除 79 | 80 | ### 使用 WebDAV 协议连接网盘 81 | 82 | 这里以坚果云作为演示 83 | 84 | 1. 打开设置界面 85 | 2. 点击“远程管理” 86 | 3. 点击“WebDAV” 87 | 4. 打开坚果云官网:`https://www.jianguoyun.com/#/safety` 88 | 5. 点击“添加应用”获得应用密码 89 | 90 | ![坚果云](../asset/坚果云.png) 91 | 92 | 6. 输入对应的信息 93 | 7. 点击“测试连接” 94 | 8. 点击“保存配置” 95 | 96 | # 可能的问题 97 | 98 | ## 什么是关键字? 99 | 100 | 关键字可以理解为是搜索算法查找对应项的唯一标识。 101 | 102 | ## 什么是遍历深度? 103 | 104 | 使用下图来表示:以选择了 `C:\Program Files\` 为例,depth = 5。 105 | 106 | ``` 107 | 初始路径:C:\Program Files\ (深度5层) 108 | ├── App1/ ✔️ 索引(第1层) 109 | │ └── Subfolder/ ✔️ 索引(第2层) 110 | │ ├── Config/ ✔️ 索引(第3层) 111 | │ └── Cache/ ✔️ 索引(第3层) 112 | └── App2/ 113 | └── Components/ 114 | └── Plugins/ 115 | └── Legacy/ 116 | └── Layer5/ ✔️ 索引(第5层) 117 | └── Layer6 ❌ 忽略(超出深度) 118 | ``` 119 | 120 | ## 程序崩了 121 | 122 | 程序的日志保存在了`C:\Users\[当前用户名]\AppData\Roaming\ZeroLaunch-rs`文件夹下,`logs`下保存着应用的启动日志与崩溃记录。 123 | 124 | ## 快捷键被占用了 125 | 126 | 打开系统托盘,找到 `ZeroLaunch-rs` 的缩略图,右击打开二级菜单栏,点击“重新注册快捷键”即可。 -------------------------------------------------------------------------------- /doc/Feature_Implementation_Guide_cn2.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # 常見功能的實現方法 4 | 5 | ## 有一些不會被使用的程式被添加了 6 | 7 | 常見的,不會被使用的程式有: 8 | * 各類程式的卸載程式 9 | * 各類程式的說明文件 10 | 11 | 這些程式已預設添加到屏蔽字中,若有些未被包含的程式也被添加了,可透過以下方式完成程式的屏蔽: 12 | 1. 開啟設定介面 13 | 2. 點擊「程式搜尋」 14 | 3. 點擊「設定屏蔽關鍵字」 15 | 4. 點擊「新增項目」 16 | 5. 輸入要屏蔽的程式 17 | 6. 點擊「儲存設定檔」,程式會自動儲存當前配置並重新載入設定 18 | 19 | 只要程式中出現關鍵字,則直接屏蔽。因此寫入 `help` 時,可將所有 `xxx help` 程式全部屏蔽 20 | 21 | ## 更改了安裝路徑的程式沒有被檢測到 22 | 23 | 程式會遍歷預設安裝路徑下的所有應用。若有自訂安裝路徑未被檢測到,可透過以下方式添加: 24 | 1. 開啟設定介面 25 | 2. 點擊「程式搜尋」 26 | 3. 點擊「設定遍歷路徑」 27 | 4. 點擊「新增項目」 28 | 5. 將安裝目錄添加並設定對應的遍歷深度 29 | 6. 點擊「儲存設定檔」,程式會自動儲存當前配置並重新載入設定 30 | 31 | 可能問題:[什麼是遍歷深度](#什麼是遍歷深度) 32 | 33 | ## 添加網址/命令(可自訂開啟Windows設定與各類控制台) 34 | 35 | 1. 開啟設定介面 36 | 2. 點擊「其他搜尋」 37 | 3. 點擊對應的標籤頁 38 | 4. 完成添加 39 | 5. 點擊「儲存設定檔」,程式會自動儲存當前配置並重新載入設定 40 | 41 | 可能問題:[什麼是關鍵字](#什麼是關鍵字) 42 | 43 | 添加Windows設定的方法:使用命令:`explorer.exe ms-settings:[目標]`,可在網上查詢`ms-settings`支援的設定。以顯示設定為例:`explorer.exe ms-settings:display` 44 | 45 | 添加各類控制台:使用` Get-ChildItem -Path C:\Windows\system32\* -Include *.msc | Sort-Object -Property Extension | Select-Object -Property Name | Format-Wide -Column 1`可查看支援的控制台。使用命令:`mmc [目標控制台]`。以本機群組原則編輯器 `gpedit.msc` 為例:使用命令:`mmc gpedit.msc` 46 | 47 | ## 對搜尋算法的微調怎麼做 48 | 49 | 首先要對這個搜尋算法的處理流程有一定了解。建議配合程式碼查看,對應的程式碼實現在`src-tauri/src/modules/program_manager/mod.rs`,更新搜尋算法的函式為`update`。 50 | 51 | 這個搜尋算法的核心思路是:對於使用者的輸入,每一個程式都有一個「匹配值」,而這個匹配值表示使用者預期目標為當前程式的可能性。匹配值越大,則表示使用者的目標程式越有可能是當前的程式。因此結果欄顯示的也是所有程式中匹配值最大的幾個。 52 | 53 | 一個程式的匹配值由以下幾個部分組成:字串匹配值 + 固定權重 + 動態權重。 54 | * **字串匹配值**:由使用者輸入的字串與搜尋關鍵字計算而來(固定變化)。 55 | * **固定權重**:使用者設定的目標程式的固定權重(由使用者決定)。 56 | * **動態權重**:根據歷史啟動次數計算而來(動態變化)。 57 | 58 | 而使用者可以更改**固定權重**的數值。注意,固定權重的賦值與**屏蔽關鍵字**一樣。 59 | 60 | 變更其值的方式如下所示: 61 | 62 | 1. 開啟設定介面 63 | 2. 點擊「程序搜索」 64 | 3. 點擊「设置固定偏移量」 65 | 4. 點擊「添加项目」 66 | 5. 設定對應的值 67 | 6. 點擊「保存配置文件」 68 | 69 | 70 | ## 設定檔保存地址的更換 71 | 72 | ### 更換本機儲存路徑 73 | 74 | 1. 開啟設定介面 75 | 2. 點擊「远程管理」 76 | 3. 點擊「本地存储」 77 | 4. 點擊「选择路径」按鈕 78 | 5. 選擇目標資料夾,成功後設定將自動儲存至新路徑 79 | 6. 點擊「测试连接」 80 | 7. 點擊「保存配置」 81 | 82 | 連線測試時會在目標資料夾建立測試檔案,可手動刪除。 83 | 84 | ### 使用 WebDAV 協議連接雲端硬碟 85 | 86 | 以堅果雲為操作示範: 87 | 1. 開啟設定介面 88 | 2. 點擊「远程管理」 89 | 3. 點擊「WebDAV」 90 | 4. 前往堅果雲官網:`https://www.jianguoyun.com/#/safety` 91 | 5. 點擊「添加应用」以取得應用程式密碼 92 | 93 | ![堅果雲](../asset/坚果云.png) 94 | 95 | 6. 輸入對應資訊 96 | 7. 點擊「测试连接」 97 | 8. 點擊「保存配置」 98 | 99 | # 可能的問題 100 | 101 | ## 什麼是關鍵字? 102 | 103 | 關鍵字可理解為搜尋演算法查找對應項的唯一識別碼。 104 | 105 | ## 什麼是遍歷深度? 106 | 107 | 用下圖表示:以選擇 `C:\Program Files\` 為例,depth = 5。 108 | 109 | ``` 110 | 初始路徑:C:\Program Files\ (深度5層) 111 | ├── App1/ ✔️ 索引(第1層) 112 | │ └── Subfolder/ ✔️ 索引(第2層) 113 | │ ├── Config/ ✔️ 索引(第3層) 114 | │ └── Cache/ ✔️ 索引(第3層) 115 | └── App2/ 116 | └── Components/ 117 | └── Plugins/ 118 | └── Legacy/ 119 | └── Layer5/ ✔️ 索引(第5層) 120 | └── Layer6 ❌ 忽略(超出深度) 121 | ``` 122 | 123 | ## 程式崩潰了 124 | 125 | 程式的日誌儲存在`C:\Users\[目前使用者名稱]\AppData\Roaming\ZeroLaunch-rs`資料夾下,`logs`下儲存著應用的啟動日誌與崩潰記錄。 126 | 127 | ## 快捷鍵被佔用了 128 | 129 | 打開系統托盤,找到 `ZeroLaunch-rs` 的縮略圖,右擊打開二級選單欄,點擊“重新註冊快捷鍵”即可。 130 | 131 | **以上內容由 DeepSeek-R1 完成轉換** -------------------------------------------------------------------------------- /doc/变量命名规范.md: -------------------------------------------------------------------------------- 1 | 1. 变量和函数名: 2 | - 使用蛇形命名法(snake_case) 3 | - 全小写,单词间用下划线分隔 4 | 5 | 示例: 6 | ```rust 7 | let user_name = "Alice"; 8 | fn calculate_total_price(item_count: i32, price_per_item: f64) -> f64 { 9 | // 函数实现 10 | } 11 | ``` 12 | 13 | 2. 常量: 14 | - 使用全大写的蛇形命名法(SCREAMING_SNAKE_CASE) 15 | - 所有字母大写,单词间用下划线分隔 16 | 17 | 示例: 18 | ```rust 19 | const MAX_CONNECTIONS: i32 = 100; 20 | const PI: f64 = 3.14159; 21 | ``` 22 | 23 | 3. 类型(结构体、枚举、特征): 24 | - 使用大驼峰命名法(PascalCase) 25 | - 每个单词的首字母大写,不使用下划线 26 | 27 | 示例: 28 | ```rust 29 | struct UserProfile { 30 | // 结构体字段 31 | } 32 | 33 | enum HttpStatus { 34 | Ok, 35 | NotFound, 36 | InternalServerError, 37 | } 38 | 39 | trait DatabaseConnection { 40 | // 特征方法 41 | } 42 | ``` 43 | 44 | 4. 宏: 45 | - 通常使用蛇形命名法,但也可以使用大驼峰命名法 46 | - 避免使用感叹号(!)作为宏名称的一部分 47 | 48 | 示例: 49 | ```rust 50 | macro_rules! vec_of_strings { 51 | // 宏定义 52 | } 53 | 54 | // 或者 55 | macro_rules! VecOfStrings { 56 | // 宏定义 57 | } 58 | ``` 59 | 60 | 5. 模块: 61 | - 使用蛇形命名法 62 | - 通常使用单个小写单词 63 | 64 | 示例: 65 | ```rust 66 | mod network { 67 | // 模块内容 68 | } 69 | 70 | mod file_system { 71 | // 模块内容 72 | } 73 | ``` 74 | 75 | 6. 类型参数(泛型): 76 | - 通常使用单个大写字母 77 | - 如果需要多个字母,可以使用大驼峰命名法 78 | 79 | 示例: 80 | ```rust 81 | struct Queue { 82 | // 结构体定义 83 | } 84 | 85 | fn process(input: InputType) -> OutputType { 86 | // 函数实现 87 | } 88 | ``` 89 | 90 | 7. 生命周期参数: 91 | - 使用小写字母,通常很短 92 | - 常用 'a, 'b, 'c 等 93 | 94 | 示例: 95 | ```rust 96 | struct Reference<'a, T> { 97 | data: &'a T, 98 | } 99 | 100 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { 101 | // 函数实现 102 | } 103 | ``` 104 | 105 | 8. 方法: 106 | - 对于getter方法,直接使用字段名(不加get前缀) 107 | - 对于setter方法,使用set_前缀加字段名 108 | 109 | 示例: 110 | ```rust 111 | impl Rectangle { 112 | fn width(&self) -> f64 { 113 | self.width 114 | } 115 | 116 | fn set_width(&mut self, width: f64) { 117 | self.width = width; 118 | } 119 | } 120 | ``` 121 | 122 | 9. 关联函数(类似于静态方法): 123 | - 通常使用new作为构造函数名 124 | - 其他关联函数使用描述性的动词 125 | 126 | 示例: 127 | ```rust 128 | impl Circle { 129 | fn new(radius: f64) -> Circle { 130 | // 构造函数实现 131 | } 132 | 133 | fn calculate_area(radius: f64) -> f64 { 134 | // 面积计算实现 135 | } 136 | } 137 | ``` 138 | 139 | 10. 测试函数: 140 | - 使用蛇形命名法 141 | - 名称应该清晰地描述测试的目的 142 | 143 | 示例: 144 | ```rust 145 | #[test] 146 | fn test_add_positive_numbers() { 147 | // 测试实现 148 | } 149 | 150 | #[test] 151 | fn test_vector_sorting() { 152 | // 测试实现 153 | } 154 | ``` 155 | 156 | 11. 源代码文件(.rs文件): 157 | - 使用蛇形命名法(snake_case) 158 | - 全小写,单词间用下划线分隔 159 | 160 | 示例: 161 | ``` 162 | main.rs 163 | user_authentication.rs 164 | database_connection.rs 165 | ``` 166 | 167 | 12. 目录(文件夹): 168 | - 通常也使用蛇形命名法 169 | - 全小写,单词间用下划线分隔 170 | 171 | 示例: 172 | ``` 173 | src/ 174 | tests/ 175 | examples/ 176 | config_files/ 177 | ``` 178 | 179 | 13. 模块文件: 180 | - 如果一个模块对应一个文件,文件名应与模块名相同 181 | - 使用蛇形命名法 182 | 183 | 示例: 184 | 如果有一个名为 `user_management` 的模块,对应的文件名应该是: 185 | ``` 186 | user_management.rs 187 | ``` 188 | 189 | 14. 测试文件: 190 | - 通常在文件名后加上 `_test` 后缀 191 | - 使用蛇形命名法 192 | 193 | 示例: 194 | ``` 195 | user_authentication_test.rs 196 | database_connection_test.rs 197 | ``` 198 | 199 | 15. 示例文件: 200 | - 通常放在 `examples` 目录下 201 | - 使用描述性的蛇形命名法 202 | 203 | 示例: 204 | ``` 205 | examples/ 206 | ├── simple_web_server.rs 207 | ├── file_io_example.rs 208 | └── multithreading_demo.rs 209 | ``` 210 | 211 | 16. 二进制 crate 文件: 212 | - 如果项目包含多个二进制 crate,通常将它们放在 `src/bin` 目录下 213 | - 每个文件对应一个可执行文件,使用蛇形命名法 214 | 215 | 示例: 216 | ``` 217 | src/ 218 | └── bin/ 219 | ├── client.rs 220 | └── server.rs 221 | ``` 222 | 223 | 17. 库 crate 的主文件: 224 | - 通常命名为 `lib.rs` 225 | 226 | 示例: 227 | ``` 228 | src/ 229 | └── lib.rs 230 | ``` 231 | 232 | 18. 主程序文件: 233 | - 通常命名为 `main.rs` 234 | 235 | 示例: 236 | ``` 237 | src/ 238 | └── main.rs 239 | ``` 240 | 241 | 19. 配置文件: 242 | - 通常使用全小写,可以包含连字符或下划线 243 | 244 | 示例: 245 | ``` 246 | config.toml 247 | rust-toolchain.toml 248 | cargo.toml 249 | .gitignore 250 | ``` 251 | 252 | 20. README 文件: 253 | - 通常使用全大写 254 | 255 | 示例: 256 | ``` 257 | README.md 258 | ``` 259 | 260 | 典型的 Rust 项目结构可能如下所示: 261 | 262 | ``` 263 | my_rust_project/ 264 | ├── Cargo.toml 265 | ├── README.md 266 | ├── src/ 267 | │ ├── main.rs 268 | │ ├── lib.rs 269 | │ ├── user_management.rs 270 | │ └── database/ 271 | │ ├── connection.rs 272 | │ └── queries.rs 273 | ├── tests/ 274 | │ └── integration_tests.rs 275 | ├── examples/ 276 | │ └── simple_usage.rs 277 | └── benches/ 278 | └── performance_benchmark.rs 279 | ``` -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Tauri + Vue + Typescript App 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /manifests/g/ghost-him/ZeroLaunch-rs/0.4.14/ghost-him.ZeroLaunch-rs.installer.yaml: -------------------------------------------------------------------------------- 1 | # Created using wingetcreate 1.10.2.0 2 | # yaml-language-server: $schema=https://aka.ms/winget-manifest.installer.1.10.0.schema.json 3 | 4 | PackageIdentifier: ghost-him.ZeroLaunch-rs 5 | PackageVersion: 0.4.14 6 | InstallerLocale: en-US 7 | InstallerType: wix 8 | ProductCode: '{0F999DC7-1EAD-49F3-9551-8BF37C3D85BA}' 9 | Installers: 10 | - Architecture: x64 11 | InstallerUrl: https://github.com/ghost-him/ZeroLaunch-rs/releases/download/v0.4.14/zerolaunch-rs_0.4.14_x64_en-US.msi 12 | InstallerSha256: 7A13BF194DC68440ABFA440BD4D490B495E4B1AD2245D91683B1B2BBE0881DE0 13 | - Architecture: arm64 14 | InstallerUrl: https://github.com/ghost-him/ZeroLaunch-rs/releases/download/v0.4.14/zerolaunch-rs_0.4.14_arm64_en-US.msi 15 | InstallerSha256: 7FCF38F794A164B4D40F5A04814B7BB21ACE04EF6826A0021806F06AA3CD0AED 16 | ManifestType: installer 17 | ManifestVersion: 1.10.0 18 | -------------------------------------------------------------------------------- /manifests/g/ghost-him/ZeroLaunch-rs/0.4.14/ghost-him.ZeroLaunch-rs.locale.en-US.yaml: -------------------------------------------------------------------------------- 1 | # Created using wingetcreate 1.10.2.0 2 | # yaml-language-server: $schema=https://aka.ms/winget-manifest.defaultLocale.1.10.0.schema.json 3 | 4 | PackageIdentifier: ghost-him.ZeroLaunch-rs 5 | PackageVersion: 0.4.14 6 | PackageLocale: en-US 7 | Publisher: ghost-him 8 | PublisherUrl: https://github.com/ghost-him 9 | PublisherSupportUrl: https://github.com/ghost-him/ZeroLaunch-rs/issues 10 | PrivacyUrl: https://github.com/ghost-him/ZeroLaunch-rs/blob/main/PRIVACY.md 11 | Author: ghost-him 12 | PackageName: ZeroLaunch-rs 13 | PackageUrl: https://github.com/ghost-him/ZeroLaunch-rs 14 | License: GPL-3.0 license 15 | LicenseUrl: https://github.com/ghost-him/ZeroLaunch-rs/blob/main/LICENSE.txt 16 | Copyright: Copyright (c) 2024-2025 ghost-him 17 | CopyrightUrl: https://github.com/ghost-him/ZeroLaunch-rs/blob/main/LICENSE.txt 18 | ShortDescription: 一个可以容忍错别字的 Windows 应用程序启动器! 19 | Description: 打错字照样秒开应用!ZeroLaunch 是一款纯粹专注的 Windows 应用程序启动器,拥有智能纠错能力和毫秒级的极速响应。它基于 Rust、Tauri 和 Vue.js 构建,旨在提供极致的性能和纯粹的体验。主要特性包括:高效智能搜索,得益于独创的算法,程序在全称/拼音/首字母三重匹配的基础上具备卓越的拼写纠错能力,并支持中英文混合查询;隐私至上,软件完全离线运行,零数据采集,您的所有数据和设置都严格保留在本地设备中;轻巧纯粹,专注于应用程序搜索与启动,无任何冗余复杂的功能,开箱即用;高度可定制,外观界面支持高度自定义,包括背景、颜色、字体、窗口大小等,并支持微调搜索算法以满足个性化需求。核心功能还包括:快速搜索并启动普通应用及 UWP 应用,智能唤醒已打开的程序窗口,支持自定义添加程序、文件、网页搜索和系统命令,智能加载程序和 Steam 游戏图标,以及防止游戏时误触的游戏模式。 20 | Moniker: zerolaunch 21 | Tags: 22 | - launcher 23 | - utools 24 | - listary 25 | - powertoys 26 | - wox 27 | - launchy 28 | - productivity 29 | - search 30 | - utility 31 | - fast 32 | - pinyin 33 | - fuzzy-search 34 | - qidongqi 35 | - rust 36 | - tauri 37 | ReleaseNotes: '更新日志:版本兼容:版本兼容至 v0.4.7,升级后无法回退,注意核对自己软件的版本,防止更新后出现配置丢失的情况。功能更新:支持svg格式的网站图标(比如github)功能优化:修复了多显示器下的bug(github #10)' 38 | ReleaseNotesUrl: https://github.com/ghost-him/ZeroLaunch-rs/releases/tag/v0.4.14 39 | InstallationNotes: 欢迎使用 ZeroLaunch-rs !如果使用过程中遇到什么问题,欢迎到Issue中提问! 40 | ManifestType: defaultLocale 41 | ManifestVersion: 1.10.0 42 | -------------------------------------------------------------------------------- /manifests/g/ghost-him/ZeroLaunch-rs/0.4.14/ghost-him.ZeroLaunch-rs.yaml: -------------------------------------------------------------------------------- 1 | # Created using wingetcreate 1.10.2.0 2 | # yaml-language-server: $schema=https://aka.ms/winget-manifest.version.1.10.0.schema.json 3 | 4 | PackageIdentifier: ghost-him.ZeroLaunch-rs 5 | PackageVersion: 0.4.14 6 | DefaultLocale: en-US 7 | ManifestType: version 8 | ManifestVersion: 1.10.0 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zerolaunch-rs", 3 | "private": true, 4 | "version": "0.5.2", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vue-tsc --noEmit && vite build", 9 | "preview": "vite preview", 10 | "tauri": "tauri", 11 | "prepare": "bun x husky || echo 'Husky not installed'" 12 | }, 13 | "dependencies": { 14 | "@element-plus/icons-vue": "^2.3.2", 15 | "@tauri-apps/api": "^2.8.0", 16 | "@tauri-apps/plugin-deep-link": "~2.4.3", 17 | "@tauri-apps/plugin-dialog": "^2.4.0", 18 | "@tauri-apps/plugin-notification": "~2.3.1", 19 | "@tauri-apps/plugin-shell": "~2.3.1", 20 | "element-plus": "^2.11.1", 21 | "lodash-es": "^4.17.21", 22 | "pinia": "^3.0.3", 23 | "vue": "^3.5.21", 24 | "vue-i18n": "^11.1.11", 25 | "vue-router": "^4.5.1" 26 | }, 27 | "devDependencies": { 28 | "@commitlint/cli": "^20.1.0", 29 | "@commitlint/config-conventional": "^20.0.0", 30 | "@tauri-apps/cli": "^2.8.4", 31 | "@types/node": "^24.3.0", 32 | "@vitejs/plugin-vue": "^6.0.1", 33 | "husky": "^9.1.7", 34 | "typescript": "^5.9.2", 35 | "vite": "^7.1.4", 36 | "vue-tsc": "^3.0.6" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /public/tauri.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src-tauri/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Generated by Tauri 6 | # will have schema files for capabilities auto-completion 7 | /gen/schemas 8 | /locales 9 | /EmbeddingGemma-300m 10 | /models/EmbeddingGemma-300m -------------------------------------------------------------------------------- /src-tauri/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "zerolaunch-rs" 3 | version = "0.5.2" 4 | description = "🚀 Lightning-fast, accurate, lightweight & pure Windows application launcher!" 5 | authors = ["ghost-him"] 6 | edition = "2021" 7 | 8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 9 | 10 | [lib] 11 | # The `_lib` suffix may seem redundant but it is necessary 12 | # to make the lib name unique and wouldn't conflict with the bin name. 13 | # This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519 14 | name = "zerolaunch_rs_lib" 15 | crate-type = ["staticlib", "cdylib", "rlib"] 16 | 17 | [build-dependencies] 18 | tauri-build = { version = "^2", features = [] } 19 | 20 | [dependencies] 21 | windows = { version = "0.61.3", features = [ 22 | "Win32_UI_WindowsAndMessaging", 23 | "Win32_Foundation", 24 | "Win32_Graphics_Gdi", 25 | "Management_Deployment", 26 | "Win32_System_Threading", 27 | "Foundation", 28 | "Win32_Security", 29 | "Foundation_Collections", 30 | "System", 31 | "Win32_System_Com", 32 | "Win32_System_Com_StructuredStorage", 33 | "Win32_UI_Shell", 34 | "Storage_Streams", 35 | "Win32_UI_Shell_PropertiesSystem", 36 | "Win32_System_Variant", 37 | "Win32_System_Registry", 38 | "Win32_Storage_FileSystem", 39 | "Win32_System_Console", 40 | "Win32_System_Diagnostics", 41 | "Win32_System_Diagnostics_ToolHelp", 42 | "Win32_Graphics_Dwm", 43 | "Win32_UI_Controls", 44 | "Win32_Networking", 45 | "Win32_Networking_WinInet", 46 | "Win32_Globalization", 47 | "Win32_System_LibraryLoader", 48 | "Win32_System_Environment" 49 | ] } 50 | 51 | tauri = { version = "^2", features = ["tray-icon", "image-ico", "image-png"] } 52 | serde = { version = "^1", features = ["derive"] } 53 | serde_json = "^1" 54 | lazy_static = "^1.5.0" 55 | rdev = { version = "^0.5.3", features = ["unstable_grab"] } 56 | widestring = "^1.2.0" 57 | windows-core = "^0.61.2" 58 | rayon = "^1.11.0" 59 | base64 = "^0.22.1" 60 | dashmap = "^6.1.0" 61 | image = "^0.25.8" 62 | tracing = "^0.1.41" 63 | tracing-subscriber = { version = "^0.3.20", features = ["env-filter"] } 64 | tracing-appender = "^0.2.3" 65 | chrono = "^0.4.41" 66 | parking_lot = "^0.12.4" 67 | tauri-plugin-global-shortcut = "^2.3.0" 68 | tauri-plugin-dialog = { version = "^2.4.0" } 69 | timer = "^0.2.0" 70 | backtrace = "^0.3.75" 71 | tauri-plugin-notification = "^2" 72 | kmeans_colors = "^0.7.0" 73 | palette = "^0.7.6" 74 | rand = "^0.9.2" 75 | anyhow = "1.0.99" 76 | reqwest = { version = "0.12.23", features = ["json"] } 77 | lnk = "0.6.3" 78 | async-trait = "0.1.89" 79 | tokio = { version = "1.47.1", features = ["full"] } 80 | reqwest_dav = "0.2.2" 81 | tauri-plugin-deep-link = "2" 82 | winreg = "0.55.0" 83 | tauri-utils = { version = "2.7.0", features = ["schema"] } 84 | device_query = "4.0.1" 85 | globset = "0.4.16" 86 | regex = "1.11.2" 87 | scraper = "0.24.0" 88 | url = "2.5.7" 89 | fnv = "1.0.7" 90 | fontdb = "0.23.0" 91 | time = "0.3.43" 92 | resvg = "0.45.1" 93 | usvg = "0.45.1" 94 | tiny-skia = "0.11.4" 95 | encoding_rs = "0.8.35" 96 | tauri-plugin-shell = "2" 97 | fuzzy-matcher = "0.3.7" 98 | ini = "1.3.0" 99 | thiserror = "2.0.16" 100 | tempfile = "3.21.0" 101 | tokio-util = "0.7.16" 102 | walkdir = "2.5.0" 103 | ort = { version = "2.0.0-rc.10", features = ["xnnpack"], optional = true } 104 | tokenizers = { version = "0.22.0", default-features = false, features = ["onig"], optional = true } 105 | ndarray = { version = "0.16.1", optional = true } 106 | once_cell = "1.21.3" 107 | lru = "0.16.1" 108 | bincode = "2.0.1" 109 | 110 | [features] 111 | default = ["custom-protocol"] 112 | custom-protocol = ["tauri/custom-protocol"] 113 | portable = [] 114 | ai = [ 115 | "dep:ort", 116 | "dep:tokenizers", 117 | "dep:ndarray", 118 | ] 119 | 120 | [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] 121 | tauri-plugin-autostart = "^2" 122 | tauri-plugin-single-instance = { version = "2", features = ["deep-link"] } 123 | 124 | [patch.crates-io] 125 | tray-icon = { git = "https://github.com/tauri-apps/tray-icon.git", rev = "34a3442" } 126 | -------------------------------------------------------------------------------- /src-tauri/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | tauri_build::build() 3 | } 4 | -------------------------------------------------------------------------------- /src-tauri/capabilities/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../gen/schemas/desktop-schema.json", 3 | "identifier": "default", 4 | "description": "Capability for the main window", 5 | "windows": [ 6 | "main", 7 | "setting_window", 8 | "welcome" 9 | ], 10 | "permissions": [ 11 | "core:default", 12 | "autostart:allow-enable", 13 | "autostart:allow-disable", 14 | "autostart:allow-is-enabled", 15 | "global-shortcut:allow-is-registered", 16 | "global-shortcut:allow-register-all", 17 | "global-shortcut:allow-unregister-all", 18 | "global-shortcut:allow-register", 19 | "global-shortcut:allow-unregister", 20 | "dialog:allow-open", 21 | "dialog:default", 22 | "core:window:default", 23 | "core:window:allow-start-dragging", 24 | "shell:allow-open" 25 | ] 26 | } -------------------------------------------------------------------------------- /src-tauri/capabilities/desktop.json: -------------------------------------------------------------------------------- 1 | { 2 | "identifier": "desktop-capability", 3 | "platforms": [ 4 | "macOS", 5 | "windows", 6 | "linux" 7 | ], 8 | "permissions": [ 9 | "autostart:default" 10 | ] 11 | } -------------------------------------------------------------------------------- /src-tauri/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghost-him/ZeroLaunch-rs/414cb78f11ff003b0b8e9e6355c30abd767d70cc/src-tauri/icons/128x128.png -------------------------------------------------------------------------------- /src-tauri/icons/128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghost-him/ZeroLaunch-rs/414cb78f11ff003b0b8e9e6355c30abd767d70cc/src-tauri/icons/128x128@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/32x32-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghost-him/ZeroLaunch-rs/414cb78f11ff003b0b8e9e6355c30abd767d70cc/src-tauri/icons/32x32-white.png -------------------------------------------------------------------------------- /src-tauri/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghost-him/ZeroLaunch-rs/414cb78f11ff003b0b8e9e6355c30abd767d70cc/src-tauri/icons/32x32.png -------------------------------------------------------------------------------- /src-tauri/icons/Square107x107Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghost-him/ZeroLaunch-rs/414cb78f11ff003b0b8e9e6355c30abd767d70cc/src-tauri/icons/Square107x107Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square142x142Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghost-him/ZeroLaunch-rs/414cb78f11ff003b0b8e9e6355c30abd767d70cc/src-tauri/icons/Square142x142Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square150x150Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghost-him/ZeroLaunch-rs/414cb78f11ff003b0b8e9e6355c30abd767d70cc/src-tauri/icons/Square150x150Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square284x284Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghost-him/ZeroLaunch-rs/414cb78f11ff003b0b8e9e6355c30abd767d70cc/src-tauri/icons/Square284x284Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square30x30Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghost-him/ZeroLaunch-rs/414cb78f11ff003b0b8e9e6355c30abd767d70cc/src-tauri/icons/Square30x30Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square310x310Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghost-him/ZeroLaunch-rs/414cb78f11ff003b0b8e9e6355c30abd767d70cc/src-tauri/icons/Square310x310Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square44x44Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghost-him/ZeroLaunch-rs/414cb78f11ff003b0b8e9e6355c30abd767d70cc/src-tauri/icons/Square44x44Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square71x71Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghost-him/ZeroLaunch-rs/414cb78f11ff003b0b8e9e6355c30abd767d70cc/src-tauri/icons/Square71x71Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square89x89Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghost-him/ZeroLaunch-rs/414cb78f11ff003b0b8e9e6355c30abd767d70cc/src-tauri/icons/Square89x89Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/StoreLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghost-him/ZeroLaunch-rs/414cb78f11ff003b0b8e9e6355c30abd767d70cc/src-tauri/icons/StoreLogo.png -------------------------------------------------------------------------------- /src-tauri/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghost-him/ZeroLaunch-rs/414cb78f11ff003b0b8e9e6355c30abd767d70cc/src-tauri/icons/icon.icns -------------------------------------------------------------------------------- /src-tauri/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghost-him/ZeroLaunch-rs/414cb78f11ff003b0b8e9e6355c30abd767d70cc/src-tauri/icons/icon.ico -------------------------------------------------------------------------------- /src-tauri/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghost-him/ZeroLaunch-rs/414cb78f11ff003b0b8e9e6355c30abd767d70cc/src-tauri/icons/icon.png -------------------------------------------------------------------------------- /src-tauri/icons/terminal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghost-him/ZeroLaunch-rs/414cb78f11ff003b0b8e9e6355c30abd767d70cc/src-tauri/icons/terminal.png -------------------------------------------------------------------------------- /src-tauri/icons/tips.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghost-him/ZeroLaunch-rs/414cb78f11ff003b0b8e9e6355c30abd767d70cc/src-tauri/icons/tips.png -------------------------------------------------------------------------------- /src-tauri/icons/web_pages.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghost-him/ZeroLaunch-rs/414cb78f11ff003b0b8e9e6355c30abd767d70cc/src-tauri/icons/web_pages.png -------------------------------------------------------------------------------- /src-tauri/models/readme.md: -------------------------------------------------------------------------------- 1 | 将下载好的模型文件解压后放到此文件夹中即可 2 | 將下載好的模型文件解壓後放到此文件夾中即可 3 | After unzipping the downloaded model file, place it in this folder. 4 | 5 | 6 | 目录结构树如下: 7 | 目錄結構樹如下: 8 | The directory structure tree is as follows: 9 | 10 | ``` 11 | models\ 12 | └─EmbeddingGemma-300m\ 13 | ``` -------------------------------------------------------------------------------- /src-tauri/src/commands/config_file.rs: -------------------------------------------------------------------------------- 1 | //use crate::core::storage::onedrive::get_onedrive_refresh_token; 2 | use crate::core::storage::storage_manager::check_validation; 3 | use crate::modules::config::config_manager::PartialRuntimeConfig; 4 | use crate::modules::config::default::REMOTE_CONFIG_DEFAULT; 5 | use crate::modules::config::load_string_to_runtime_config_; 6 | use crate::save_config_to_file; 7 | use crate::storage::config::PartialLocalConfig; 8 | use crate::update_app_setting; 9 | use crate::AppState; 10 | use crate::REMOTE_CONFIG_NAME; 11 | use std::sync::Arc; 12 | use tauri::Emitter; 13 | use tauri::Manager; 14 | use tauri::Runtime; 15 | use tracing::error; 16 | 17 | /// 更新程序管理器的路径配置 18 | #[tauri::command] 19 | pub async fn command_save_remote_config( 20 | _app: tauri::AppHandle, 21 | state: tauri::State<'_, Arc>, 22 | partial_config: PartialRuntimeConfig, 23 | ) -> Result<(), String> { 24 | use tracing::info; 25 | 26 | info!("💾 开始保存远程配置"); 27 | 28 | let runtime_config = state.get_runtime_config(); 29 | 30 | runtime_config.update(partial_config); 31 | info!("✅ 运行时配置已更新"); 32 | 33 | save_config_to_file(true).await; 34 | info!("💾 远程配置保存完成"); 35 | 36 | Ok(()) 37 | } 38 | 39 | #[tauri::command] 40 | pub async fn command_load_local_config( 41 | _app: tauri::AppHandle, 42 | _window: tauri::Window, 43 | state: tauri::State<'_, Arc>, 44 | ) -> Result { 45 | use tracing::info; 46 | 47 | info!("📂 开始加载本地配置"); 48 | 49 | let storage_manager = state.get_storage_manager(); 50 | 51 | let config = storage_manager.to_partial().await; 52 | info!("✅ 本地配置加载完成"); 53 | 54 | Ok(config) 55 | } 56 | 57 | #[tauri::command] 58 | pub async fn command_save_local_config( 59 | app: tauri::AppHandle, 60 | _window: tauri::Window, 61 | state: tauri::State<'_, Arc>, 62 | partial_config: PartialLocalConfig, 63 | ) -> Result<(), String> { 64 | use tracing::{debug, info, warn}; 65 | 66 | info!("💾 开始保存本地配置"); 67 | 68 | let storage_manager = state.get_storage_manager(); 69 | 70 | debug!("📤 强制上传所有文件"); 71 | storage_manager.upload_all_file_force().await; 72 | 73 | debug!("🔄 更新存储管理器配置"); 74 | storage_manager.update(partial_config).await; 75 | 76 | let runtime_config = state.get_runtime_config(); 77 | 78 | debug!("📥 获取远程配置数据"); 79 | let remote_config_data = { 80 | if let Some(data) = storage_manager 81 | .download_file_str(REMOTE_CONFIG_NAME.to_string()) 82 | .await 83 | { 84 | debug!("✅ 从远程下载配置成功"); 85 | data 86 | } else { 87 | debug!("📤 远程配置不存在,上传默认配置"); 88 | storage_manager 89 | .upload_file_str( 90 | REMOTE_CONFIG_NAME.to_string(), 91 | REMOTE_CONFIG_DEFAULT.clone(), 92 | ) 93 | .await; 94 | REMOTE_CONFIG_DEFAULT.clone() 95 | } 96 | }; 97 | 98 | debug!("🔄 加载并更新运行时配置"); 99 | let partial_config = load_string_to_runtime_config_(&remote_config_data); 100 | runtime_config.update(partial_config); 101 | 102 | debug!("⚙️ 更新应用设置"); 103 | update_app_setting().await; 104 | 105 | let setting_window = match app.get_webview_window("setting_window") { 106 | Some(window) => window, 107 | None => { 108 | warn!("❌ 获取设置窗口失败"); 109 | return Err("Failed to get setting window".to_string()); 110 | } 111 | }; 112 | 113 | if let Err(e) = setting_window.emit("emit_update_setting_window_config", "") { 114 | error!("向 setting_window 发送信号失败: {:?}", e); 115 | } else { 116 | debug!("📡 设置窗口更新信号发送成功"); 117 | } 118 | 119 | info!("✅ 本地配置保存完成"); 120 | Ok(()) 121 | } 122 | 123 | #[tauri::command] 124 | pub async fn command_check_validation( 125 | _app: tauri::AppHandle, 126 | _window: tauri::Window, 127 | partial_config: PartialLocalConfig, 128 | ) -> Result, String> { 129 | Ok(check_validation(partial_config).await) 130 | } 131 | 132 | // #[tauri::command] 133 | // pub async fn command_get_onedrive_refresh_token( 134 | // app: tauri::AppHandle, 135 | // window: tauri::Window, 136 | // ) -> Result { 137 | // get_onedrive_refresh_token(window).await 138 | // } 139 | -------------------------------------------------------------------------------- /src-tauri/src/commands/debug.rs: -------------------------------------------------------------------------------- 1 | use crate::program_manager::unit::SearchTestResult; 2 | use crate::state::app_state::AppState; 3 | use std::sync::Arc; 4 | /// 这个页面存放用于测试的代码 5 | use tauri::Runtime; 6 | 7 | #[tauri::command] 8 | pub async fn test_search_algorithm( 9 | _app: tauri::AppHandle, 10 | _window: tauri::Window, 11 | state: tauri::State<'_, Arc>, 12 | search_text: String, 13 | ) -> Result, String> { 14 | let program_manager = state.get_program_manager(); 15 | Ok(program_manager.test_search_algorithm(&search_text).await) 16 | } 17 | 18 | #[tauri::command] 19 | pub async fn test_search_algorithm_time( 20 | _app: tauri::AppHandle, 21 | _window: tauri::Window, 22 | state: tauri::State<'_, Arc>, 23 | ) -> Result<(f64, f64, f64), String> { 24 | let program_manager = state.get_program_manager(); 25 | Ok(program_manager.test_search_algorithm_time().await) 26 | } 27 | 28 | #[tauri::command] 29 | pub async fn test_index_app_time( 30 | _app: tauri::AppHandle, 31 | _window: tauri::Window, 32 | state: tauri::State<'_, Arc>, 33 | ) -> Result { 34 | let program_manager = state.get_program_manager(); 35 | let time = program_manager.get_program_loader_loading_time().await; 36 | Ok(time) 37 | } 38 | 39 | #[tauri::command] 40 | pub async fn get_search_keys( 41 | _app: tauri::AppHandle, 42 | _window: tauri::Window, 43 | state: tauri::State<'_, Arc>, 44 | show_name: String, 45 | ) -> Result, String> { 46 | let program_manager = state.get_program_manager(); 47 | let search_keywords = program_manager.get_search_keywords(&show_name).await; 48 | Ok(search_keywords) 49 | } 50 | -------------------------------------------------------------------------------- /src-tauri/src/commands/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod config_file; 2 | pub mod debug; 3 | pub mod program_service; 4 | pub mod shortcut; 5 | pub mod ui_command; 6 | pub mod utils; 7 | -------------------------------------------------------------------------------- /src-tauri/src/commands/shortcut.rs: -------------------------------------------------------------------------------- 1 | use crate::AppState; 2 | use std::sync::Arc; 3 | use tauri::Runtime; 4 | 5 | #[tauri::command] 6 | pub async fn command_unregister_all_shortcut( 7 | _app: tauri::AppHandle, 8 | _window: tauri::Window, 9 | state: tauri::State<'_, Arc>, 10 | ) -> Result<(), String> { 11 | if state.get_game_mode() { 12 | return Err("请关闭游戏模式后再更改".to_string()); 13 | } 14 | let shortcut_manager = state.get_shortcut_manager(); 15 | shortcut_manager.unregister_all_shortcut() 16 | } 17 | 18 | #[tauri::command] 19 | pub async fn command_register_all_shortcut( 20 | _app: tauri::AppHandle, 21 | _window: tauri::Window, 22 | state: tauri::State<'_, Arc>, 23 | ) -> Result<(), String> { 24 | let shortcut_manager = state.get_shortcut_manager(); 25 | shortcut_manager.register_all_shortcuts() 26 | } 27 | -------------------------------------------------------------------------------- /src-tauri/src/commands/utils.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use crate::core::storage::utils::read_str; 4 | use crate::core::storage::windows_utils::get_default_remote_data_dir_path; 5 | use crate::error::ResultExt; 6 | use crate::modules::version_checker::VersionChecker; 7 | use crate::utils::font_database::get_fonts; 8 | 9 | /// 获得当前程序的最新版本 10 | #[tauri::command] 11 | pub async fn command_get_latest_release_version() -> String { 12 | let result = VersionChecker::get_latest_release_version().await; 13 | match result { 14 | Ok(data) => data, 15 | Err(e) => e.to_string(), 16 | } 17 | } 18 | 19 | /// 获得默认的远程配置文件保存地址 20 | #[tauri::command] 21 | pub fn command_get_default_remote_data_dir_path() -> String { 22 | get_default_remote_data_dir_path() 23 | } 24 | 25 | /// 获取当前的字体 26 | /// 虽然后端向前端发送的是HashSet,但是 tauri 会在传输的过程中将这个变量一个普通的数组,所以前端可以使用string[]来接收 27 | #[tauri::command] 28 | pub fn command_get_system_fonts() -> HashSet { 29 | get_fonts() 30 | } 31 | 32 | #[tauri::command] 33 | pub async fn command_read_file(path: String) -> String { 34 | let result = tauri::async_runtime::spawn_blocking(move || { 35 | // 在这里可以安全地执行阻塞代码 36 | read_str(&path).expect_programming(&format!("读取当前文件失败:{}", &path)) 37 | }) 38 | .await; 39 | result.expect_programming("读取文件失败") 40 | } 41 | -------------------------------------------------------------------------------- /src-tauri/src/core/ai/ai_loader.rs: -------------------------------------------------------------------------------- 1 | // ai模型加载器 2 | use crate::core::ai::embedding_model::EmbeddingModel; 3 | use crate::core::ai::embedding_model::EmbeddingModelType; 4 | use crate::core::ai::GraphOptimizationLevel; 5 | use crate::core::ai::OnnxModelConfig; 6 | use crate::error::OptionExt; 7 | use crate::Arc; 8 | use once_cell::sync::OnceCell; 9 | use ort::execution_providers::CPUExecutionProvider; 10 | use ort::execution_providers::XNNPACKExecutionProvider; 11 | use ort::session::Session; 12 | use ort::Error; 13 | use parking_lot::Mutex; 14 | use tokenizers::Tokenizer; 15 | use tracing::info; 16 | use tracing::warn; 17 | #[derive(Debug)] 18 | pub struct AILoader {} 19 | 20 | impl Default for AILoader { 21 | fn default() -> Self { 22 | Self::new() 23 | } 24 | } 25 | 26 | static ORT_INIT: OnceCell<()> = OnceCell::new(); 27 | 28 | impl AILoader { 29 | pub fn new() -> Self { 30 | // 全局只初始化一次 ORT 31 | let _ = ORT_INIT.get_or_init(|| { 32 | if let Err(e) = ort::init().commit() { 33 | warn!("Failed to initialize ORT: {:?}", e); 34 | } else { 35 | info!("ORT initialized"); 36 | } 37 | }); 38 | Self {} 39 | } 40 | 41 | // pub fn load_text_generation_model(&self, model_type: TextGenerationModelType) -> ort::Result { 42 | // match model_type { 43 | // TextGenerationModelType::DeepSeekR1Distill => { 44 | 45 | // } 46 | // } 47 | // } 48 | 49 | pub fn load_embedding_model( 50 | &self, 51 | model_type: EmbeddingModelType, 52 | ) -> ort::Result>> { 53 | model_type.generate_model() 54 | } 55 | } 56 | pub fn setup_session_and_tokenizer(config: &OnnxModelConfig) -> ort::Result<(Session, Tokenizer)> { 57 | let mut session: Option = None; 58 | // Attempt XNNPACK + CPU 59 | if session.is_none() { 60 | match Session::builder() 61 | .and_then(|b| b.with_optimization_level(GraphOptimizationLevel::Level3)) 62 | .and_then(|b| { 63 | b.with_execution_providers([ 64 | XNNPACKExecutionProvider::default().build(), 65 | CPUExecutionProvider::default().build(), 66 | ]) 67 | }) 68 | .and_then(|b| b.commit_from_file(&config.model_path)) 69 | { 70 | Ok(s) => { 71 | info!("Using execution providers: XNNPACK, CPU"); 72 | session = Some(s); 73 | } 74 | Err(e) => warn!("Failed to init XNNPACK, CPU providers: {:?}", e), 75 | } 76 | } 77 | 78 | // CPU Only 79 | if session.is_none() { 80 | info!("Falling back to CPU execution provider only"); 81 | session = Some( 82 | Session::builder()? 83 | .with_optimization_level(GraphOptimizationLevel::Level3)? 84 | .with_execution_providers([CPUExecutionProvider::default().build()])? 85 | .commit_from_file(&config.model_path)?, 86 | ); 87 | } 88 | 89 | let session = session 90 | .expect_programming("CPU only session creation should never fail if we reached here"); 91 | let tokenizer = Tokenizer::from_file(&config.tokenizer_path) 92 | .map_err(|e| Error::new(format!("Failed to load tokenizer: {}", e)))?; 93 | 94 | Ok((session, tokenizer)) 95 | } 96 | -------------------------------------------------------------------------------- /src-tauri/src/core/ai/embedding_model/embedding_gemma.rs: -------------------------------------------------------------------------------- 1 | use crate::core::ai::ai_loader::setup_session_and_tokenizer; 2 | use crate::core::ai::embedding_model::{Array1, ArrayView1}; 3 | use crate::core::ai::{embedding_model::EmbeddingModel, OnnxModelConfig}; 4 | use ndarray::Axis; 5 | use ort::session::Session; 6 | use ort::value::TensorRef; 7 | use ort::Error; 8 | use rayon::prelude::*; 9 | use serde_json::Value; 10 | use std::fmt; 11 | use tokenizers::Tokenizer; 12 | 13 | pub struct EmbeddingGemmaModel { 14 | session: Session, 15 | tokenizer: Tokenizer, 16 | config: OnnxModelConfig, 17 | } 18 | 19 | impl fmt::Debug for EmbeddingGemmaModel { 20 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 21 | write!(f, "EmbeddingGemmaModel") 22 | } 23 | } 24 | 25 | impl EmbeddingModel for EmbeddingGemmaModel { 26 | fn new(config: OnnxModelConfig) -> ort::Result { 27 | let (session, mut tokenizer) = setup_session_and_tokenizer(&config)?; 28 | 29 | // Load tokenizer config 30 | let config_content = std::fs::read_to_string(&config.tokenizer_config_path) 31 | .map_err(|e| Error::new(format!("Failed to read tokenizer config: {}", e)))?; 32 | let tokenizer_config: Value = serde_json::from_str(&config_content) 33 | .map_err(|e| Error::new(format!("Failed to parse tokenizer config: {}", e)))?; 34 | 35 | // Configure padding 36 | if let Some(pad_token) = tokenizer_config["pad_token"].as_str() { 37 | tokenizer.with_padding(Some(tokenizers::PaddingParams { 38 | strategy: tokenizers::PaddingStrategy::BatchLongest, 39 | direction: tokenizers::PaddingDirection::Right, 40 | pad_to_multiple_of: None, 41 | pad_id: 0, 42 | pad_type_id: 0, 43 | pad_token: pad_token.to_string(), 44 | })); 45 | } 46 | 47 | Ok(Self { 48 | session, 49 | tokenizer, 50 | config, 51 | }) 52 | } 53 | 54 | fn compute_embedding(&mut self, text: &str) -> ort::Result> { 55 | let embeddings = self.compute_embeddings(&[text])?; 56 | Ok(embeddings.into_iter().next().unwrap_or_default()) 57 | } 58 | 59 | fn compute_embeddings(&mut self, texts: &[&str]) -> ort::Result>> { 60 | // Tokenize inputs 61 | let encodings = self 62 | .tokenizer 63 | .encode_batch(texts.to_vec(), true) 64 | .map_err(|e| Error::new(e.to_string()))?; 65 | 66 | let padded_token_length = encodings[0].len(); 67 | 68 | // Prepare input tensors 69 | let total_tokens = texts.len() * padded_token_length; 70 | let mut ids = Vec::with_capacity(total_tokens); 71 | let mut mask = Vec::with_capacity(total_tokens); 72 | 73 | let ids_temp: Vec = encodings 74 | .par_iter() 75 | .flat_map(|e| e.get_ids().par_iter().map(|i| *i as i64)) 76 | .collect(); 77 | let mask_temp: Vec = encodings 78 | .par_iter() 79 | .flat_map(|e| e.get_attention_mask().par_iter().map(|i| *i as i64)) 80 | .collect(); 81 | ids.extend(ids_temp); 82 | mask.extend(mask_temp); 83 | 84 | // Create tensor references 85 | let a_ids = TensorRef::from_array_view(([texts.len(), padded_token_length], &*ids))?; 86 | let a_mask = TensorRef::from_array_view(([texts.len(), padded_token_length], &*mask))?; 87 | 88 | // Run inference 89 | let outputs = self.session.run(ort::inputs![a_ids, a_mask])?; 90 | let embeddings_raw = outputs[0].try_extract_array::()?.to_owned(); 91 | 92 | // Perform mean pooling 93 | let mut embeddings = embeddings_raw.mean_axis(Axis(1)).unwrap(); 94 | 95 | // L2 normalization 96 | 97 | embeddings.axis_iter_mut(Axis(0)).for_each(|mut row| { 98 | let norm = row.iter().map(|x| x * x).sum::().sqrt(); 99 | if norm > 0.0 { 100 | row.mapv_inplace(|x| x / norm); 101 | } 102 | }); 103 | 104 | // Convert to Vec> 105 | let result: Vec> = embeddings 106 | .axis_iter(Axis(0)) 107 | .map(|row| row.to_owned().into_dimensionality().unwrap()) 108 | .collect(); 109 | 110 | Ok(result) 111 | } 112 | 113 | fn compute_similarity(embedding1: ArrayView1, embedding2: ArrayView1) -> f32 { 114 | if embedding1.len() != embedding2.len() { 115 | return 0.0; 116 | } 117 | 118 | // 直接计算点积并返回百分比形式 119 | embedding1.dot(&embedding2) * 100.0 120 | } 121 | 122 | fn update_backend(&mut self, new_config: OnnxModelConfig) -> ort::Result<()> { 123 | // Create new model instance with updated config 124 | let new_model = Self::new(new_config)?; 125 | 126 | // Replace current instance 127 | self.session = new_model.session; 128 | self.tokenizer = new_model.tokenizer; 129 | self.config = new_model.config; 130 | 131 | Ok(()) 132 | } 133 | 134 | fn get_runtime_config(&self) -> &OnnxModelConfig { 135 | &self.config 136 | } 137 | 138 | fn get_default_config() -> OnnxModelConfig { 139 | OnnxModelConfig { 140 | model_path: "models/EmbeddingGemma-300m/model.onnx".to_string(), 141 | tokenizer_path: "models/EmbeddingGemma-300m/tokenizer.json".to_string(), 142 | tokenizer_config_path: "models/EmbeddingGemma-300m/tokenizer_config.json".to_string(), 143 | ..Default::default() 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src-tauri/src/core/ai/embedding_model/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod embedding_gemma; 2 | use std::fmt::Debug; 3 | 4 | use crate::core::ai::{embedding_model::embedding_gemma::EmbeddingGemmaModel, OnnxModelConfig}; 5 | use crate::Arc; 6 | use ndarray::{Array1, ArrayView1}; 7 | use parking_lot::Mutex; 8 | /// Embedding模型trait定义 9 | pub trait EmbeddingModel: Debug + Send + Sync { 10 | /// 初始化模型 11 | fn new(config: OnnxModelConfig) -> ort::Result 12 | where 13 | Self: Sized; 14 | 15 | /// 计算文本的embedding向量 16 | fn compute_embedding(&mut self, text: &str) -> ort::Result>; 17 | 18 | /// 批量计算文本的embedding向量 19 | fn compute_embeddings(&mut self, texts: &[&str]) -> ort::Result>>; 20 | 21 | /// 计算两个embedding向量的相似度(返回百分比) 22 | fn compute_similarity(embedding1: ArrayView1, embedding2: ArrayView1) -> f32 23 | where 24 | Self: Sized; 25 | 26 | /// 更新后端配置 27 | fn update_backend(&mut self, config: OnnxModelConfig) -> ort::Result<()>; 28 | 29 | /// 获取当前配置 30 | fn get_runtime_config(&self) -> &OnnxModelConfig; 31 | 32 | /// 获取模型的固定配置信息 33 | fn get_default_config() -> OnnxModelConfig 34 | where 35 | Self: Sized; 36 | } 37 | 38 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 39 | pub enum EmbeddingModelType { 40 | EmbeddingGemma, 41 | } 42 | 43 | impl EmbeddingModelType { 44 | pub fn get_config(&self) -> OnnxModelConfig { 45 | match self { 46 | EmbeddingModelType::EmbeddingGemma => EmbeddingGemmaModel::get_default_config(), 47 | } 48 | } 49 | 50 | pub fn generate_model(&self) -> ort::Result>> { 51 | match self { 52 | EmbeddingModelType::EmbeddingGemma => Ok(Arc::new(Mutex::new( 53 | EmbeddingGemmaModel::new(self.get_config())?, 54 | ))), 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src-tauri/src/core/ai/mod.rs: -------------------------------------------------------------------------------- 1 | use ort::session::builder::GraphOptimizationLevel; 2 | pub mod ai_loader; 3 | pub mod embedding_model; 4 | pub mod model_manager; 5 | pub mod text_generation_model; 6 | 7 | /// Onnx模型配置结构体 8 | #[derive(Debug)] 9 | pub struct OnnxModelConfig { 10 | /// 是否启用DirectML后端 11 | pub enable_directml: bool, 12 | /// 是否启用cuda后端 13 | pub enable_cuda: bool, 14 | /// 模型文件路径 15 | pub model_path: String, 16 | /// Tokenizer文件路径 17 | pub tokenizer_path: String, 18 | /// Tokenizer配置文件路径 19 | pub tokenizer_config_path: String, 20 | } 21 | 22 | impl Default for OnnxModelConfig { 23 | fn default() -> Self { 24 | Self { 25 | enable_directml: true, 26 | enable_cuda: true, 27 | model_path: String::new(), 28 | tokenizer_path: String::new(), 29 | tokenizer_config_path: String::new(), 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src-tauri/src/core/ai/model_manager.rs: -------------------------------------------------------------------------------- 1 | // 这个模块用于管理模型的生命周期,包括加载、卸载、缓存等操作。 2 | // 用户如果想要调用模型,则都是通过这个管理器调用的,同时只需要传入要调用什么模型,而不用管具体的加载细节与模型的类型,也不用关心模型的配置细节 3 | use crate::core::ai::ai_loader::AILoader; 4 | use crate::core::ai::embedding_model::EmbeddingModel; 5 | use crate::core::ai::embedding_model::EmbeddingModelType; 6 | use crate::Arc; 7 | use dashmap::DashMap; 8 | use parking_lot::Mutex; 9 | #[derive(Debug)] 10 | pub struct ModelManager { 11 | ai_loader: AILoader, 12 | //text_generation_models: HashMap>>, 13 | embedding_models: DashMap>>, 14 | } 15 | 16 | impl Default for ModelManager { 17 | fn default() -> Self { 18 | Self::new() 19 | } 20 | } 21 | 22 | impl ModelManager { 23 | pub fn new() -> Self { 24 | let ai_loader = AILoader::new(); 25 | Self { 26 | ai_loader, 27 | //text_generation_models: HashMap::new(), 28 | embedding_models: DashMap::new(), 29 | } 30 | } 31 | 32 | // pub fn load_text_generation_model(&self, model_type: TextGenerationModelType) -> ort::Result { 33 | // self.ai_loader.load_text_generation_model(model_type) 34 | // } 35 | 36 | pub fn load_embedding_model( 37 | &self, 38 | model_type: EmbeddingModelType, 39 | ) -> ort::Result>> { 40 | use dashmap::mapref::entry::Entry; 41 | match self.embedding_models.entry(model_type) { 42 | Entry::Occupied(e) => Ok(e.get().clone()), 43 | Entry::Vacant(v) => { 44 | let model = self.ai_loader.load_embedding_model(model_type)?; 45 | let cloned = model.clone(); 46 | v.insert(cloned.clone()); 47 | Ok(cloned) 48 | } 49 | } 50 | } 51 | 52 | pub fn release_embedding_model(&self, model_type: EmbeddingModelType) { 53 | if self.embedding_models.remove(&model_type).is_some() { 54 | tracing::debug!(?model_type, "Released embedding model from cache"); 55 | } 56 | } 57 | 58 | pub fn release_all_embedding_models(&self) { 59 | if !self.embedding_models.is_empty() { 60 | tracing::debug!("Clearing all cached embedding models"); 61 | self.embedding_models.clear(); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src-tauri/src/core/ai/text_generation_model.rs: -------------------------------------------------------------------------------- 1 | pub enum TextGenerationModelType { 2 | Qwen3, 3 | } 4 | -------------------------------------------------------------------------------- /src-tauri/src/core/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "ai")] 2 | pub mod ai; 3 | pub mod image_processor; 4 | pub mod storage; 5 | -------------------------------------------------------------------------------- /src-tauri/src/core/storage/config.rs: -------------------------------------------------------------------------------- 1 | use crate::core::storage::local_save::LocalSaveConfig; 2 | use crate::core::storage::local_save::PartialLocalSaveConfig; 3 | // use crate::core::storage::onedrive::OneDriveConfig; 4 | // use crate::core::storage::onedrive::PartialOneDriveConfig; 5 | use crate::core::storage::webdav::PartialWebDAVConfig; 6 | use crate::core::storage::webdav::WebDAVConfig; 7 | use crate::modules::config::default::APP_VERSION; 8 | use serde::{Deserialize, Serialize}; 9 | use std::sync::Arc; 10 | #[derive(Serialize, Deserialize, Debug, Clone)] 11 | pub enum StorageDestination { 12 | WebDAV, 13 | Local, 14 | OneDrive, 15 | } 16 | 17 | #[derive(Serialize, Deserialize, Debug, Clone, Default)] 18 | pub struct PartialLocalConfig { 19 | pub storage_destination: Option, 20 | pub local_save_config: Option, 21 | pub webdav_save_config: Option, 22 | //pub onedrive_save_config: Option, 23 | pub save_to_local_per_update: Option, 24 | pub version: Option, 25 | pub welcome_page_version: Option, 26 | } 27 | 28 | #[derive(Debug)] 29 | pub struct LocalConfig { 30 | // 软件的版本,用于判断当前的用户是不是更新了,默认值为空 31 | version: Arc, 32 | // 表示远程配置信息的存储的地址 33 | storage_destination: Arc, 34 | // 表示远程配置信息如果要保存在本地,则其使用的配置 35 | local_save_config: Arc, 36 | // 表示远程配置信息如果要保存的webdav服务器,则其使用的配置 37 | webdav_save_config: Arc, 38 | //onedrive_save_config: Arc, 39 | // 表示缓冲区的大小,保存几次后会更新到本地 40 | save_to_local_per_update: Arc, 41 | // 欢迎页面的版本号,用于判断是否需要显示欢迎页面 42 | welcome_page_version: Arc, 43 | } 44 | 45 | impl Default for LocalConfig { 46 | fn default() -> Self { 47 | LocalConfig { 48 | version: Arc::new(String::new()), 49 | storage_destination: Arc::new(StorageDestination::Local), 50 | local_save_config: Arc::new(LocalSaveConfig::default()), 51 | webdav_save_config: Arc::new(WebDAVConfig::default()), 52 | //onedrive_save_config: Arc::new(OneDriveConfig::default()), 53 | save_to_local_per_update: Arc::new(4), 54 | welcome_page_version: Arc::new(String::new()), 55 | } 56 | } 57 | } 58 | 59 | impl LocalConfig { 60 | pub fn get_version(&self) -> Arc { 61 | self.version.clone() 62 | } 63 | 64 | pub fn get_storage_destination(&self) -> Arc { 65 | self.storage_destination.clone() 66 | } 67 | 68 | pub fn get_local_save_config(&self) -> Arc { 69 | self.local_save_config.clone() 70 | } 71 | 72 | pub fn get_webdav_save_config(&self) -> Arc { 73 | self.webdav_save_config.clone() 74 | } 75 | 76 | // pub fn get_onedrive_save_config(&self) -> Arc { 77 | // self.onedrive_save_config.clone() 78 | // } 79 | 80 | pub fn get_save_to_local_per_update(&self) -> Arc { 81 | self.save_to_local_per_update.clone() 82 | } 83 | 84 | pub fn get_welcome_page_version(&self) -> Arc { 85 | self.welcome_page_version.clone() 86 | } 87 | 88 | pub fn update(&mut self, partial_local_config: PartialLocalConfig) { 89 | self.version = Arc::new(APP_VERSION.to_string()); 90 | if let Some(sd) = partial_local_config.storage_destination { 91 | self.storage_destination = Arc::new(sd); 92 | } 93 | if let Some(local_save_config) = partial_local_config.local_save_config { 94 | self.local_save_config.update(local_save_config); 95 | } 96 | if let Some(webdav_save_config) = partial_local_config.webdav_save_config { 97 | self.webdav_save_config.update(webdav_save_config); 98 | } 99 | // if let Some(onedrive_save_config) = partial_local_config.onedrive_save_config { 100 | // self.onedrive_save_config.update(onedrive_save_config); 101 | // } 102 | if let Some(count) = partial_local_config.save_to_local_per_update { 103 | self.save_to_local_per_update = Arc::new(count); 104 | } 105 | if let Some(welcome_version) = partial_local_config.welcome_page_version { 106 | self.welcome_page_version = Arc::new(welcome_version); 107 | } 108 | } 109 | 110 | pub fn to_partial(&self) -> PartialLocalConfig { 111 | PartialLocalConfig { 112 | storage_destination: Some((*self.storage_destination).clone()), 113 | local_save_config: Some(self.local_save_config.to_partial()), 114 | webdav_save_config: Some(self.webdav_save_config.to_partial()), 115 | //onedrive_save_config: Some(self.onedrive_save_config.to_partial()), 116 | save_to_local_per_update: Some(*self.save_to_local_per_update), 117 | version: Some((*self.version).clone()), 118 | welcome_page_version: Some((*self.welcome_page_version).clone()), 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src-tauri/src/core/storage/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod config; 2 | pub mod local_save; 3 | pub mod onedrive; 4 | pub mod storage_manager; 5 | pub mod utils; 6 | pub mod webdav; 7 | pub mod windows_utils; 8 | -------------------------------------------------------------------------------- /src-tauri/src/core/storage/utils.rs: -------------------------------------------------------------------------------- 1 | use encoding_rs; 2 | use std::fs; 3 | /// 存放通用工具函数 4 | use std::io; 5 | use std::io::Error; 6 | use std::path::Path; 7 | use tracing::warn; 8 | 9 | /// 读取一个文件,如果没有这个文件,则返回错误 10 | /// 返回一个字符串 11 | pub fn read_str(path: &str) -> Result { 12 | fs::read_to_string(path) 13 | } 14 | 15 | pub fn create_str(path: &str, content: &str) -> Result<(), String> { 16 | if let Some(parent) = Path::new(path).parent() { 17 | if let Err(e) = fs::create_dir_all(parent) { 18 | return Err(format!("无法创建文件夹: {}", e)); 19 | } 20 | } 21 | match fs::write(path, content) { 22 | Ok(_) => Ok(()), 23 | Err(e) => Err(format!("无法写入文件: {},错误: {}", path, e)), 24 | } 25 | } 26 | 27 | /// 从一个文件中读取数据,如果没有这个文件,则创建一个新的文件,并写入初始内容 28 | /// 返回一个字符串 29 | pub fn read_or_create_str(path: &str, content: Option) -> Result { 30 | match read_str(path) { 31 | Ok(data) => Ok(data), 32 | Err(error) => { 33 | if error.kind() == io::ErrorKind::NotFound { 34 | let initial_content = content.unwrap_or_default(); 35 | match create_str(path, &initial_content) { 36 | Ok(_) => Ok(initial_content), 37 | Err(write_err) => Err(format!("无法写入文件: {}", write_err)), 38 | } 39 | } else { 40 | Err(format!("无法读取文件: {}", error)) 41 | } 42 | } 43 | } 44 | } 45 | 46 | /// 读取一个文件,如果没有这个文件,则返回错误 47 | /// 返回一个字节数组 48 | pub fn read_bytes(path: &str) -> Result, Error> { 49 | fs::read(path) 50 | } 51 | 52 | /// 创建一个文件,并写入字节内容。如果文件已存在则覆盖。 53 | /// 写入前会确保父目录存在。 54 | pub fn create_bytes(path: &str, content: &[u8]) -> Result<(), String> { 55 | if let Some(parent) = Path::new(path).parent() { 56 | if let Err(e) = fs::create_dir_all(parent) { 57 | return Err(format!("无法创建文件夹: {}", e)); 58 | } 59 | } 60 | match fs::write(path, content) { 61 | Ok(_) => Ok(()), 62 | Err(e) => Err(format!("无法写入文件: {},错误: {}", path, e)), 63 | } 64 | } 65 | 66 | /// 从一个文件中读取数据,如果没有这个文件,则创建一个新的文件,并写入初始内容 67 | /// 返回一个字节数组 68 | pub fn read_or_create_bytes(path: &str, content: Option>) -> Result, String> { 69 | match read_bytes(path) { 70 | // 使用新的 read_bytes 函数 71 | Ok(data) => Ok(data), 72 | Err(error) => { 73 | if error.kind() == io::ErrorKind::NotFound { 74 | let initial_content = content.unwrap_or_default(); // 如果 content 为 None,则使用空 Vec 75 | match create_bytes(path, &initial_content) { 76 | // 使用新的 create_bytes 函数 77 | Ok(_) => Ok(initial_content), 78 | Err(write_err) => Err(format!("无法写入文件: {}", write_err)), 79 | } 80 | } else { 81 | Err(format!("无法读取文件: {}", error)) 82 | } 83 | } 84 | } 85 | } 86 | 87 | /// 将lnk解析为绝对路径 88 | /// 优先使用本地的编码,如果失败,则使用utf16编码 89 | pub fn get_lnk_target_path(lnk_path: &str) -> Option { 90 | let shell_link_result = lnk::ShellLink::open(lnk_path, encoding_rs::GB18030); 91 | 92 | let shell_link = match shell_link_result { 93 | Ok(link) => link, 94 | Err(e_gb18030) => { 95 | warn!( 96 | "Failed to open LNK file '{}' with GB18030 encoding: {:?}", 97 | lnk_path, e_gb18030 98 | ); 99 | // 2. 二次尝试:如果首次尝试失败,则使用 UTF-16LE 编码 100 | match lnk::ShellLink::open(lnk_path, encoding_rs::UTF_16LE) { 101 | Ok(link) => { 102 | warn!( 103 | "在主要编码尝试失败后,成功使用 UTF-16LE 编码打开 LNK 文件: {}", 104 | lnk_path 105 | ); 106 | link 107 | } 108 | Err(e_utf16) => { 109 | warn!( 110 | "尝试使用 UTF-16LE 编码打开 LNK 文件 '{}' 再次失败: {:?}", 111 | lnk_path, e_utf16 112 | ); 113 | return None; 114 | } 115 | } 116 | } 117 | }; 118 | 119 | // 从成功打开的 shell_link 中提取路径信息 120 | let link_info = match shell_link.link_info() { 121 | Some(info) => info, 122 | None => { 123 | warn!("无法从 LNK 文件 '{}' 获取 link_info。", lnk_path); 124 | return None; 125 | } 126 | }; 127 | 128 | match link_info.local_base_path() { 129 | Some(path) => Some(path.to_string()), 130 | None => { 131 | warn!( 132 | "无法从 LNK 文件 '{}' 获取基本路径 (local_base_path)。", 133 | lnk_path 134 | ); 135 | None 136 | } 137 | } 138 | } 139 | 140 | /// 读取一个目标目录,如果读到了,则返回数据,如果没有这个目录,则新建这个目录 141 | pub fn read_dir_or_create>(path: P) -> Result { 142 | match fs::read_dir(&path) { 143 | Ok(dir) => Ok(dir), 144 | Err(e) if e.kind() == io::ErrorKind::NotFound => { 145 | fs::create_dir_all(&path).map_err(|e| format!("无法创建目录: {}", e))?; 146 | fs::read_dir(path).map_err(|e| format!("无法读取新创建的目录: {}", e)) 147 | } 148 | Err(e) => Err(format!("无法读取目录: {}", e)), 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src-tauri/src/core/storage/windows_utils.rs: -------------------------------------------------------------------------------- 1 | use tracing::{debug, info}; 2 | use windows::Win32::UI::Shell::SHGetFolderPathW; 3 | use windows::Win32::UI::Shell::CSIDL_STARTMENU; 4 | use windows::Win32::UI::Shell::{CSIDL_COMMON_STARTMENU, CSIDL_DESKTOP}; 5 | 6 | #[cfg(not(feature = "portable"))] 7 | use crate::error::{OptionExt, ResultExt}; 8 | #[cfg(not(feature = "portable"))] 9 | use std::path::Path; 10 | #[cfg(not(feature = "portable"))] 11 | use windows::Win32::UI::Shell::{FOLDERID_RoamingAppData, SHGetKnownFolderPath, KF_FLAG_DEFAULT}; 12 | /// 获取 13 | /// 获取当前用户的桌面路径 14 | pub fn get_desktop_path() -> Result { 15 | // 创建缓冲区,足够存储路径 16 | const MAX_PATH_LEN: usize = 260; // MAX_PATH 17 | let mut desktop_path_buffer: [u16; MAX_PATH_LEN] = [0; MAX_PATH_LEN]; 18 | 19 | unsafe { 20 | // 获取用户桌面路径 21 | let hr_desktop = SHGetFolderPathW( 22 | None, // hwndOwner, 通常为 NULL 23 | CSIDL_DESKTOP as i32, // nFolder, 指定桌面文件夹 24 | None, // hToken, NULL 表示当前用户 25 | 0, // dwFlags, SHGFP_TYPE_CURRENT 26 | &mut desktop_path_buffer, // pszPath, 输出缓冲区 27 | ); 28 | 29 | if hr_desktop.is_err() { 30 | return Err(format!( 31 | "Failed to get CSIDL_DESKTOP. HRESULT: {:?}", // 以十六进制显示 HRESULT 32 | hr_desktop 33 | )); 34 | } 35 | 36 | // 将有效的宽字符缓冲区部分转换为 Rust String 37 | let desktop_path = widestring::U16CStr::from_ptr_str(&desktop_path_buffer as *const u16) 38 | .to_string() 39 | .map_err(|e| format!("Failed to convert common path to string: {:?}", e))?; 40 | 41 | debug!("用户桌面路径: {}", desktop_path); 42 | Ok(desktop_path) 43 | } 44 | } 45 | 46 | /// 获取公共和用户的开始菜单路径 47 | pub fn get_start_menu_paths() -> Result<(String, String), String> { 48 | // 创建缓冲区,足够存储路径 49 | const MAX_PATH_LEN: usize = 260; 50 | let mut common_path_buffer: [u16; MAX_PATH_LEN] = [0; MAX_PATH_LEN]; 51 | let mut user_path_buffer: [u16; MAX_PATH_LEN] = [0; MAX_PATH_LEN]; 52 | 53 | unsafe { 54 | // 获取公共开始菜单路径 55 | let hr_common = SHGetFolderPathW( 56 | None, 57 | CSIDL_COMMON_STARTMENU as i32, 58 | None, 59 | 0, 60 | &mut common_path_buffer, 61 | ); 62 | 63 | if hr_common.is_err() { 64 | return Err(format!( 65 | "Failed to get CSIDL_COMMON_STARTMENU: {:?}", 66 | hr_common 67 | )); 68 | } 69 | 70 | // 获取用户开始菜单路径 71 | let hr_user = 72 | SHGetFolderPathW(None, CSIDL_STARTMENU as i32, None, 0, &mut user_path_buffer); 73 | 74 | if hr_user.is_err() { 75 | return Err(format!("Failed to get CSIDL_STARTMENU: {:?}", hr_user)); 76 | } 77 | 78 | // 将宽字符缓冲区转换为 Rust String 79 | let common_path = widestring::U16CStr::from_ptr_str(&common_path_buffer as *const u16) 80 | .to_string() 81 | .map_err(|e| format!("Failed to convert common path to string: {:?}", e))?; 82 | 83 | let user_path = widestring::U16CStr::from_ptr_str(&user_path_buffer as *const u16) 84 | .to_string() 85 | .map_err(|e| format!("Failed to convert user path to string: {:?}", e))?; 86 | 87 | debug!("菜单路径: {common_path}, {user_path}"); 88 | Ok((common_path, user_path)) 89 | } 90 | } 91 | // 获取数据目录的路径 92 | pub fn get_default_remote_data_dir_path() -> String { 93 | #[cfg(feature = "portable")] 94 | { 95 | // 便携版本:使用程序所在目录 96 | if let Ok(exe_path) = std::env::current_exe() { 97 | if let Some(exe_dir) = exe_path.parent() { 98 | let portable_dir = exe_dir.to_string_lossy().to_string(); 99 | info!("Portable Directory: {}", portable_dir); 100 | return portable_dir; 101 | } 102 | } 103 | // 如果获取失败,回退到当前工作目录 104 | let current_dir = std::env::current_dir() 105 | .unwrap_or_else(|_| std::path::PathBuf::from(".")) 106 | .to_string_lossy() 107 | .to_string(); 108 | info!("Fallback to current directory: {}", current_dir); 109 | current_dir 110 | } 111 | 112 | #[cfg(not(feature = "portable"))] 113 | { 114 | unsafe { 115 | // 获取 AppData 目录 116 | let path = SHGetKnownFolderPath(&FOLDERID_RoamingAppData, KF_FLAG_DEFAULT, None); 117 | 118 | // 将 PWSTR 转换为 Rust 字符串 119 | let path_str = path 120 | .expect_programming("Failed to get AppData path") 121 | .to_string() 122 | .expect_programming("Failed to convert path to string"); 123 | let app_data_str = Path::new(&path_str) 124 | .join("ZeroLaunch-rs") 125 | .to_str() 126 | .expect_programming("Failed to convert path to string") 127 | .to_string(); 128 | info!("AppData Directory: {}", app_data_str); 129 | app_data_str 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src-tauri/src/main.rs: -------------------------------------------------------------------------------- 1 | // Prevents additional console window on Windows in release, DO NOT REMOVE!! 2 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] 3 | 4 | fn main() { 5 | zerolaunch_rs_lib::run() 6 | } 7 | -------------------------------------------------------------------------------- /src-tauri/src/modules/config/config_manager.rs: -------------------------------------------------------------------------------- 1 | use super::ui_config::PartialUiConfig; 2 | use crate::modules::config::app_config::AppConfig; 3 | use crate::modules::config::app_config::PartialAppConfig; 4 | use crate::modules::config::ui_config::UiConfig; 5 | use crate::modules::config::window_state::PartialWindowState; 6 | use crate::modules::config::window_state::WindowState; 7 | use crate::modules::shortcut_manager::shortcut_config::PartialShortcutConfig; 8 | use crate::modules::shortcut_manager::shortcut_config::ShortcutConfig; 9 | use crate::program_manager::config::program_manager_config::PartialProgramManagerConfig; 10 | use crate::program_manager::config::program_manager_config::ProgramManagerConfig; 11 | use serde::{Deserialize, Serialize}; 12 | use std::sync::Arc; 13 | 14 | #[derive(Serialize, Deserialize, Debug, Clone)] 15 | pub struct PartialRuntimeConfig { 16 | pub app_config: Option, 17 | pub ui_config: Option, 18 | pub shortcut_config: Option, 19 | pub program_manager_config: Option, 20 | pub window_state: Option, 21 | } 22 | 23 | #[derive(Debug)] 24 | pub struct RuntimeConfig { 25 | app_config: Arc, 26 | ui_config: Arc, 27 | shortcut_config: Arc, 28 | program_manager_config: Arc, 29 | window_state: Arc, 30 | } 31 | 32 | impl Default for RuntimeConfig { 33 | fn default() -> Self { 34 | Self::new() 35 | } 36 | } 37 | 38 | impl RuntimeConfig { 39 | pub fn new() -> Self { 40 | RuntimeConfig { 41 | app_config: Arc::new(AppConfig::default()), 42 | ui_config: Arc::new(UiConfig::default()), 43 | shortcut_config: Arc::new(ShortcutConfig::default()), 44 | program_manager_config: Arc::new(ProgramManagerConfig::default()), 45 | window_state: Arc::new(WindowState::default()), 46 | } 47 | } 48 | 49 | pub fn update(&self, partial_config: PartialRuntimeConfig) { 50 | if let Some(partial_app_config) = partial_config.app_config { 51 | self.app_config.update(partial_app_config); 52 | } 53 | if let Some(partial_ui_config) = partial_config.ui_config { 54 | self.ui_config.update(partial_ui_config); 55 | } 56 | if let Some(shortcut_config) = partial_config.shortcut_config { 57 | self.shortcut_config.update(shortcut_config); 58 | } 59 | if let Some(partial_program_manager_config) = partial_config.program_manager_config { 60 | self.program_manager_config 61 | .update(partial_program_manager_config); 62 | } 63 | if let Some(partial_window_state) = partial_config.window_state { 64 | self.window_state.update(partial_window_state); 65 | } 66 | } 67 | 68 | pub fn get_app_config(&self) -> Arc { 69 | self.app_config.clone() 70 | } 71 | 72 | pub fn get_ui_config(&self) -> Arc { 73 | self.ui_config.clone() 74 | } 75 | 76 | pub fn get_shortcut_config(&self) -> Arc { 77 | self.shortcut_config.clone() 78 | } 79 | 80 | pub fn get_program_manager_config(&self) -> Arc { 81 | self.program_manager_config.clone() 82 | } 83 | 84 | pub fn get_window_state(&self) -> Arc { 85 | self.window_state.clone() 86 | } 87 | 88 | pub fn to_partial(&self) -> PartialRuntimeConfig { 89 | PartialRuntimeConfig { 90 | app_config: Some(self.app_config.to_partial()), 91 | ui_config: Some(self.ui_config.to_partial()), 92 | shortcut_config: Some(self.shortcut_config.to_partial()), 93 | program_manager_config: Some(self.program_manager_config.to_partial()), 94 | window_state: None, 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src-tauri/src/modules/config/default.rs: -------------------------------------------------------------------------------- 1 | use crate::core::storage::windows_utils::get_default_remote_data_dir_path; 2 | use crate::error::{OptionExt, ResultExt}; 3 | use crate::RuntimeConfig; 4 | use dashmap::DashMap; 5 | use lazy_static::lazy_static; 6 | use std::path::Path; 7 | // 这里存放的都是在程序初始化以后就不会再改变的变量 8 | lazy_static! { 9 | static ref DATA_DIR_PATH: String = get_default_remote_data_dir_path(); 10 | 11 | pub static ref LOCAL_CONFIG_PATH: String = { 12 | Path::new(&*DATA_DIR_PATH) 13 | .join("ZeroLaunch_local_config.json") 14 | .to_str() 15 | .expect_programming("Failed to convert path to string") 16 | .to_string() 17 | }; 18 | /// 日志文件夹的路径 19 | pub static ref LOG_DIR: String = { 20 | Path::new(&*DATA_DIR_PATH) 21 | .join("logs") 22 | .to_str() 23 | .expect_programming("Failed to convert path to string") 24 | .to_string() 25 | }; 26 | /// 图标缓存文件夹的路径 27 | pub static ref ICON_CACHE_DIR: String = { 28 | Path::new(&*DATA_DIR_PATH) 29 | .join("icons") 30 | .to_str() 31 | .expect_programming("Failed to convert path to string") 32 | .to_string() 33 | }; 34 | /// 模型文件的保存路径(与应用程序同级目录下的 models) 35 | pub static ref MODELS_DIR: String = { 36 | // 优先使用可执行文件所在目录;失败时回退到当前工作目录 37 | let exe_dir = std::env::current_exe() 38 | .ok() 39 | .and_then(|p| p.parent().map(|d| d.to_path_buf())) 40 | .unwrap_or_else(|| { 41 | std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")) 42 | }); 43 | 44 | exe_dir 45 | .join("models") 46 | .to_str() 47 | .expect_programming("Failed to convert path to string") 48 | .to_string() 49 | }; 50 | /// app使用到的图片的路径 51 | pub static ref APP_PIC_PATH: DashMap = DashMap::new(); 52 | /// 默认的配置信息 53 | pub static ref REMOTE_CONFIG_DEFAULT: String = serde_json::to_string(&RuntimeConfig::new().to_partial()) 54 | .expect_programming("Failed to serialize default runtime config"); 55 | /// 当前软件的版本号 56 | pub static ref APP_VERSION: String = env!("CARGO_PKG_VERSION").to_string(); 57 | 58 | } 59 | 60 | pub const REMOTE_CONFIG_NAME: &str = "ZeroLaunch_remote_config.json"; 61 | 62 | pub const SEMANTIC_DESCRIPTION_FILE_NAME: &str = "ZeroLaunch_program_semantic_description.json"; 63 | 64 | /// 程序embedding缓存的二进制文件名 65 | pub const SEMANTIC_EMBEDDING_CACHE_FILE_NAME: &str = "ZeroLaunch_program_embeddings.cache"; 66 | 67 | pub const PINYIN_CONTENT_JS: &str = include_str!("../program_manager/pinyin.json"); 68 | -------------------------------------------------------------------------------- /src-tauri/src/modules/config/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::error::ResultExt; 2 | use config_manager::PartialRuntimeConfig; 3 | 4 | pub mod app_config; 5 | pub mod config_manager; 6 | pub mod default; 7 | pub mod ui_config; 8 | pub mod window_state; 9 | 10 | pub type Width = i32; 11 | pub type Height = i32; 12 | use crate::RuntimeConfig; 13 | use serde::{Deserialize, Serialize}; 14 | #[derive(Serialize, Deserialize, Debug, Clone)] 15 | pub struct LocalConfig { 16 | pub version: String, 17 | pub config_data: PartialRuntimeConfig, 18 | } 19 | 20 | // 当前配置文件的版本 21 | impl LocalConfig { 22 | pub const CURRENT_VERSION: &str = "2"; 23 | } 24 | 25 | pub fn save_runtime_config_to_string(partial_config: PartialRuntimeConfig) -> String { 26 | let data = LocalConfig { 27 | version: LocalConfig::CURRENT_VERSION.to_string(), 28 | config_data: partial_config, 29 | }; 30 | 31 | serde_json::to_string(&data).expect_programming("Failed to serialize local config") 32 | } 33 | 34 | pub fn load_string_to_runtime_config_(local_config_data: &str) -> PartialRuntimeConfig { 35 | // 读取配置文件 36 | let final_config: PartialRuntimeConfig; 37 | match serde_json::from_str::(local_config_data) { 38 | Ok(config) => { 39 | // 如果已经正常的读到文件了,则判断文件是不是正常读取了 40 | if config.version == LocalConfig::CURRENT_VERSION { 41 | final_config = config.config_data; 42 | } else { 43 | final_config = RuntimeConfig::new().to_partial(); 44 | } 45 | } 46 | Err(_e) => { 47 | final_config = RuntimeConfig::new().to_partial(); 48 | } 49 | } 50 | final_config 51 | } 52 | -------------------------------------------------------------------------------- /src-tauri/src/modules/config/window_state.rs: -------------------------------------------------------------------------------- 1 | use crate::modules::config::{Height, Width}; 2 | use parking_lot::RwLock; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | #[derive(Serialize, Deserialize, Debug, Clone)] 6 | pub struct PartialWindowState { 7 | pub sys_window_scale_factor: Option, 8 | pub sys_window_width: Option, 9 | pub sys_window_height: Option, 10 | pub sys_window_locate_width: Option, 11 | pub sys_window_locate_height: Option, 12 | } 13 | 14 | /// 表示当前程序所在的屏幕的信息,比如第一个屏幕,第二个屏幕 15 | #[derive(Debug)] 16 | struct WindowStateInner { 17 | /// 当前屏幕的缩放比例 18 | sys_window_scale_factor: f64, 19 | /// 显示器的宽 20 | sys_window_width: Width, 21 | /// 显示器的长 22 | sys_window_height: Height, 23 | /// 所在显示器的起点的宽(可能所在的显示器为第二个显示器) 24 | sys_window_locate_width: Width, 25 | /// 所在显示器的起点的高 26 | sys_window_locate_height: Height, 27 | } 28 | 29 | impl Default for WindowStateInner { 30 | fn default() -> Self { 31 | WindowStateInner { 32 | sys_window_scale_factor: 1.0, 33 | sys_window_width: 0, 34 | sys_window_height: 0, 35 | sys_window_locate_width: 0, 36 | sys_window_locate_height: 0, 37 | } 38 | } 39 | } 40 | 41 | impl WindowStateInner { 42 | pub fn get_sys_window_scale_factor(&self) -> f64 { 43 | self.sys_window_scale_factor 44 | } 45 | /// 显示器的宽 46 | pub fn get_sys_window_width(&self) -> Width { 47 | self.sys_window_width 48 | } 49 | /// 显示器的长 50 | pub fn get_sys_window_height(&self) -> Height { 51 | self.sys_window_height 52 | } 53 | /// 所在显示器的起点的宽(可能所在的显示器为第二个显示器) 54 | pub fn get_sys_window_locate_width(&self) -> Width { 55 | self.sys_window_locate_width 56 | } 57 | /// 所在显示器的起点的高 58 | pub fn get_sys_window_locate_height(&self) -> Height { 59 | self.sys_window_locate_height 60 | } 61 | } 62 | #[derive(Debug)] 63 | pub struct WindowState { 64 | inner: RwLock, 65 | } 66 | 67 | impl Default for WindowState { 68 | fn default() -> Self { 69 | WindowState { 70 | inner: RwLock::new(WindowStateInner::default()), 71 | } 72 | } 73 | } 74 | 75 | impl WindowState { 76 | pub fn get_sys_window_scale_factor(&self) -> f64 { 77 | let inner = self.inner.read(); 78 | inner.get_sys_window_scale_factor() 79 | } 80 | /// 显示器的宽 81 | pub fn get_sys_window_width(&self) -> Width { 82 | let inner = self.inner.read(); 83 | inner.get_sys_window_width() 84 | } 85 | /// 显示器的长 86 | pub fn get_sys_window_height(&self) -> Height { 87 | let inner = self.inner.read(); 88 | inner.get_sys_window_height() 89 | } 90 | 91 | /// 所在显示器的起点的宽(可能所在的显示器为第二个显示器) 92 | pub fn get_sys_window_locate_width(&self) -> Width { 93 | let inner = self.inner.read(); 94 | inner.get_sys_window_locate_width() 95 | } 96 | /// 所在显示器的起点的高 97 | pub fn get_sys_window_locate_height(&self) -> Height { 98 | let inner = self.inner.read(); 99 | inner.get_sys_window_locate_height() 100 | } 101 | 102 | pub fn update(&self, partial_window_state: PartialWindowState) { 103 | let mut inner = self.inner.write(); 104 | if let Some(sys_window_scale_factor) = partial_window_state.sys_window_scale_factor { 105 | inner.sys_window_scale_factor = sys_window_scale_factor; 106 | } 107 | 108 | if let Some(sys_window_height) = partial_window_state.sys_window_height { 109 | inner.sys_window_height = sys_window_height; 110 | } 111 | 112 | if let Some(sys_window_width) = partial_window_state.sys_window_width { 113 | inner.sys_window_width = sys_window_width; 114 | } 115 | 116 | if let Some(sys_window_locate_width) = partial_window_state.sys_window_locate_width { 117 | inner.sys_window_locate_width = sys_window_locate_width; 118 | } 119 | 120 | if let Some(sys_window_locate_height) = partial_window_state.sys_window_locate_height { 121 | inner.sys_window_locate_height = sys_window_locate_height; 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src-tauri/src/modules/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod config; 2 | pub mod program_manager; 3 | pub mod shortcut_manager; 4 | pub mod ui_controller; 5 | pub mod version_checker; 6 | -------------------------------------------------------------------------------- /src-tauri/src/modules/program_manager/config/image_loader_config.rs: -------------------------------------------------------------------------------- 1 | use parking_lot::RwLock; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | pub struct RuntimeImageLoaderConfig { 5 | /// 默认的 app 图标的路径 6 | pub default_app_icon_path: String, 7 | /// 默认的 网址图标 8 | pub default_web_icon_path: String, 9 | } 10 | 11 | #[derive(Serialize, Deserialize, Debug, Clone)] 12 | pub struct PartialImageLoaderConfig { 13 | pub enable_icon_cache: Option, 14 | pub enable_online: Option, 15 | } 16 | 17 | #[derive(Serialize, Deserialize, Debug, Clone)] 18 | #[serde(default)] 19 | pub struct ImageLoaderConfigInner { 20 | /// 要不要开启图片缓存 21 | #[serde(default = "ImageLoaderConfigInner::default_enable_icon_cache")] 22 | pub enable_icon_cache: bool, 23 | /// 要不要联网来获取网址的图标 24 | #[serde(default = "ImageLoaderConfigInner::default_enable_online")] 25 | pub enable_online: bool, 26 | } 27 | 28 | impl Default for ImageLoaderConfigInner { 29 | fn default() -> Self { 30 | Self { 31 | enable_icon_cache: Self::default_enable_icon_cache(), 32 | enable_online: Self::default_enable_online(), 33 | } 34 | } 35 | } 36 | 37 | impl ImageLoaderConfigInner { 38 | pub(crate) fn default_enable_icon_cache() -> bool { 39 | true 40 | } 41 | 42 | pub(crate) fn default_enable_online() -> bool { 43 | true 44 | } 45 | } 46 | 47 | impl ImageLoaderConfigInner { 48 | pub fn to_partial(&self) -> PartialImageLoaderConfig { 49 | PartialImageLoaderConfig { 50 | enable_icon_cache: Some(self.enable_icon_cache), 51 | enable_online: Some(self.enable_online), 52 | } 53 | } 54 | 55 | pub fn update(&mut self, partial_config: PartialImageLoaderConfig) { 56 | if let Some(enable) = partial_config.enable_icon_cache { 57 | self.enable_icon_cache = enable; 58 | } 59 | if let Some(enable) = partial_config.enable_online { 60 | self.enable_online = enable; 61 | } 62 | } 63 | } 64 | 65 | #[derive(Debug)] 66 | pub struct ImageLoaderConfig { 67 | inner: RwLock, 68 | } 69 | 70 | impl Default for ImageLoaderConfig { 71 | fn default() -> Self { 72 | ImageLoaderConfig { 73 | inner: RwLock::new(ImageLoaderConfigInner::default()), 74 | } 75 | } 76 | } 77 | 78 | impl ImageLoaderConfig { 79 | pub fn to_partial(&self) -> PartialImageLoaderConfig { 80 | let inner = self.inner.read(); 81 | inner.to_partial() 82 | } 83 | 84 | pub fn get_enable_icon_cache(&self) -> bool { 85 | self.inner.read().enable_icon_cache 86 | } 87 | 88 | pub fn get_enable_online(&self) -> bool { 89 | self.inner.read().enable_online 90 | } 91 | 92 | pub fn update(&self, partial_config: PartialImageLoaderConfig) { 93 | let mut inner = self.inner.write(); 94 | inner.update(partial_config); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src-tauri/src/modules/program_manager/config/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod image_loader_config; 2 | pub mod program_loader_config; 3 | pub mod program_manager_config; 4 | pub mod program_ranker_config; 5 | -------------------------------------------------------------------------------- /src-tauri/src/modules/program_manager/config/program_manager_config.rs: -------------------------------------------------------------------------------- 1 | use super::image_loader_config::PartialImageLoaderConfig; 2 | use super::image_loader_config::RuntimeImageLoaderConfig; 3 | use crate::modules::program_manager::config::image_loader_config::ImageLoaderConfig; 4 | use crate::modules::program_manager::config::program_ranker_config::PartialProgramRankerConfig; 5 | use crate::modules::program_manager::config::program_ranker_config::ProgramRankerConfig; 6 | use crate::modules::program_manager::semantic_manager::EmbeddingBackend; 7 | use crate::program_manager::config::program_loader_config::PartialProgramLoaderConfig; 8 | use crate::program_manager::config::program_loader_config::ProgramLoaderConfig; 9 | use crate::program_manager::SearchModelConfig; 10 | use parking_lot::RwLock; 11 | use serde::{Deserialize, Serialize}; 12 | use std::sync::Arc; 13 | 14 | #[derive(Serialize, Deserialize, Debug, Clone)] 15 | pub struct PartialProgramManagerConfig { 16 | pub ranker: Option, 17 | pub loader: Option, 18 | pub image_loader: Option, 19 | pub search_model: Option>, 20 | pub enable_lru_search_cache: Option, 21 | pub search_cache_capacity: Option, 22 | } 23 | 24 | #[derive(Debug)] 25 | pub struct ProgramManagerConfigInner { 26 | pub ranker_config: Arc, 27 | pub loader_config: Arc, 28 | pub image_loader: Arc, 29 | pub search_model: Arc, 30 | pub enable_lru_search_cache: bool, 31 | pub search_cache_capacity: usize, 32 | } 33 | 34 | impl Default for ProgramManagerConfigInner { 35 | fn default() -> Self { 36 | ProgramManagerConfigInner { 37 | ranker_config: Arc::new(ProgramRankerConfig::default()), 38 | loader_config: Arc::new(ProgramLoaderConfig::default()), 39 | image_loader: Arc::new(ImageLoaderConfig::default()), 40 | search_model: Arc::new(SearchModelConfig::default()), 41 | enable_lru_search_cache: false, 42 | search_cache_capacity: 120, 43 | } 44 | } 45 | } 46 | 47 | impl ProgramManagerConfigInner { 48 | pub fn to_partial(&self) -> PartialProgramManagerConfig { 49 | PartialProgramManagerConfig { 50 | ranker: Some(self.ranker_config.to_partial()), 51 | loader: Some(self.loader_config.to_partial()), 52 | image_loader: Some(self.image_loader.to_partial()), 53 | search_model: Some(self.search_model.clone()), 54 | enable_lru_search_cache: Some(self.enable_lru_search_cache), 55 | search_cache_capacity: Some(self.search_cache_capacity), 56 | } 57 | } 58 | pub fn update(&mut self, partial_config: PartialProgramManagerConfig) { 59 | if let Some(partial_ranker) = partial_config.ranker { 60 | self.ranker_config.update(partial_ranker); 61 | } 62 | if let Some(partial_loader) = partial_config.loader { 63 | self.loader_config.update(partial_loader); 64 | } 65 | if let Some(partial_image_loader) = partial_config.image_loader { 66 | self.image_loader.update(partial_image_loader); 67 | } 68 | if let Some(new_search_model) = partial_config.search_model { 69 | self.search_model = new_search_model; 70 | } 71 | if let Some(enable_cache) = partial_config.enable_lru_search_cache { 72 | self.enable_lru_search_cache = enable_cache; 73 | } 74 | if let Some(capacity) = partial_config.search_cache_capacity { 75 | if capacity == 0 { 76 | self.search_cache_capacity = 1; 77 | } else { 78 | self.search_cache_capacity = capacity; 79 | } 80 | } 81 | } 82 | } 83 | #[derive(Debug)] 84 | pub struct ProgramManagerConfig { 85 | inner: RwLock, 86 | } 87 | 88 | impl Default for ProgramManagerConfig { 89 | fn default() -> Self { 90 | ProgramManagerConfig { 91 | inner: RwLock::new(ProgramManagerConfigInner::default()), 92 | } 93 | } 94 | } 95 | 96 | impl ProgramManagerConfig { 97 | pub fn to_partial(&self) -> PartialProgramManagerConfig { 98 | let inner = self.inner.read(); 99 | inner.to_partial() 100 | } 101 | 102 | pub fn get_ranker_config(&self) -> Arc { 103 | self.inner.read().ranker_config.clone() 104 | } 105 | 106 | pub fn get_loader_config(&self) -> Arc { 107 | self.inner.read().loader_config.clone() 108 | } 109 | 110 | pub fn get_image_loader_config(&self) -> Arc { 111 | self.inner.read().image_loader.clone() 112 | } 113 | 114 | pub fn get_search_model_config(&self) -> Arc { 115 | self.inner.read().search_model.clone() 116 | } 117 | 118 | pub fn is_lru_search_cache_enabled(&self) -> bool { 119 | self.inner.read().enable_lru_search_cache 120 | } 121 | 122 | pub fn get_search_cache_capacity(&self) -> usize { 123 | self.inner.read().search_cache_capacity 124 | } 125 | 126 | pub fn update(&self, partial_config: PartialProgramManagerConfig) { 127 | let mut inner = self.inner.write(); 128 | inner.update(partial_config); 129 | } 130 | } 131 | 132 | /// 运行时的配置信息,只会在程序初始化时被传入类,用于初始化相关的组件 133 | pub struct RuntimeProgramConfig { 134 | /// 图片加载器的配置 135 | pub image_loader_config: RuntimeImageLoaderConfig, 136 | /// 语义搜索后端(启用 AI 时存在) 137 | pub embedding_backend: Option>, 138 | /// 启动时加载到内存的embedding缓存(二进制) 139 | pub embedding_cache_bytes: Option>, 140 | } 141 | -------------------------------------------------------------------------------- /src-tauri/src/modules/program_manager/localization_translation.rs: -------------------------------------------------------------------------------- 1 | use crate::error::{OptionExt, ResultExt}; 2 | use crate::utils::windows::expand_environment_variables; 3 | use crate::Path; 4 | use ini::inistr; 5 | use std::collections::HashMap; 6 | use std::ffi::OsStr; 7 | use std::os::windows::ffi::OsStrExt; 8 | use tracing::warn; 9 | use windows::Win32::Foundation::FreeLibrary; 10 | use windows::Win32::System::LibraryLoader::{LoadLibraryExW, LOAD_LIBRARY_AS_DATAFILE}; 11 | use windows::Win32::UI::WindowsAndMessaging::LoadStringW; 12 | use windows_core::{PCWSTR, PWSTR}; 13 | /// 解析形如 "@C:\path\to\file.dll,-12345" 的资源字符串。 14 | /// 15 | /// # Arguments 16 | /// * `resource_ref` - 从 ini 文件读取到的原始值。 17 | /// 18 | /// # Returns 19 | /// `Some(String)` 如果成功解析出本地化字符串,否则 `None`。 20 | fn resolve_resource_string(resource_ref: &str) -> Option { 21 | // 1. 验证和解析输入字符串 22 | // 确保以 '@' 开头,并且至少还有一个 ',' 和一个数字 23 | if !resource_ref.starts_with('@') { 24 | return None; 25 | } 26 | 27 | // 去掉开头的 '@' 28 | let s = &resource_ref[1..]; 29 | 30 | // 找到最后一个逗号来分离路径和ID 31 | let comma_pos = s.rfind(',')?; 32 | 33 | let (path_part, id_part_with_comma) = s.split_at(comma_pos); 34 | 35 | // 展开环境变量,如 %SystemRoot% 36 | let expanded_path = match expand_environment_variables(path_part) { 37 | Some(path) => path, 38 | None => { 39 | return None; 40 | } 41 | }; 42 | 43 | let resource_id = match id_part_with_comma[1..].parse::() { 44 | Ok(id) => id.unsigned_abs(), 45 | Err(e) => { 46 | warn!("Failed to parse resource ID: {}", e); 47 | return None; 48 | } 49 | }; 50 | 51 | // 2. 调用 Windows API 52 | unsafe { 53 | let wide_path: Vec = OsStr::new(&expanded_path) 54 | .encode_wide() 55 | .chain(std::iter::once(0)) 56 | .collect(); 57 | 58 | let lib_handle = match LoadLibraryExW( 59 | PCWSTR::from_raw(wide_path.as_ptr()), 60 | None, 61 | LOAD_LIBRARY_AS_DATAFILE, 62 | ) { 63 | Ok(handle) => handle, 64 | Err(e) => { 65 | warn!("Failed to load library {}: {:?}", expanded_path, e); 66 | return None; 67 | } 68 | }; 69 | 70 | let mut buffer: [u16; 512] = [0; 512]; 71 | let length = LoadStringW( 72 | Some(lib_handle.into()), 73 | resource_id, 74 | PWSTR::from_raw(buffer.as_mut_ptr()), 75 | buffer.len() as i32, 76 | ); 77 | 78 | let _ = FreeLibrary(lib_handle); 79 | 80 | if length > 0 { 81 | return Some(String::from_utf16_lossy(&buffer[..length as usize])); 82 | } 83 | } 84 | 85 | None 86 | } 87 | /// 解析指定目录下的 desktop.ini 文件,提取 [LocalizedFileNames] 部分。 88 | /// 它会自动解析 DLL 资源引用。 89 | /// 返回一个从原始文件名到本地化名称的映射。 90 | pub fn parse_localized_names_from_dir(dir_path: &Path) -> HashMap { 91 | let ini_path = dir_path.join("desktop.ini"); 92 | if !ini_path.exists() { 93 | return HashMap::new(); 94 | } 95 | 96 | // 处理 desktop.ini 可能的 UTF-16 编码 97 | let content = match std::fs::read(&ini_path) { 98 | Ok(bytes) => { 99 | // 检查 UTF-16LE BOM (FF FE) 100 | if bytes.starts_with(&[0xFF, 0xFE]) && bytes.len() >= 2 { 101 | // 跳过 BOM (2个字节) 102 | let u16_bytes = &bytes[2..]; 103 | 104 | // 确保字节数是偶数,否则最后一个字节会被忽略 105 | if u16_bytes.len() % 2 != 0 { 106 | // 或者返回错误,或者记录日志 107 | return HashMap::new(); 108 | } 109 | 110 | let utf16_values: Vec = u16_bytes 111 | .chunks_exact(2) // 将切片按每2个字节分组 112 | .map(|chunk| { 113 | u16::from_le_bytes( 114 | chunk 115 | .try_into() 116 | .expect_programming("Chunk should be exactly 2 bytes"), 117 | ) 118 | }) // 将 [u8; 2] 转换为小端序 u16 119 | .collect(); 120 | 121 | widestring::U16Str::from_slice(&utf16_values).to_string_lossy() 122 | } else { 123 | String::from_utf8_lossy(&bytes).to_string() 124 | } 125 | } 126 | Err(e) => { 127 | warn!("Failed to read desktop.ini file: {}", e); 128 | return HashMap::new(); 129 | } 130 | }; 131 | 132 | let conf = inistr!(&content); 133 | let mut localized_map = HashMap::new(); 134 | if let Some(section) = conf.get("localizedfilenames") { 135 | for (key, value) in section.iter() { 136 | if value.is_none() { 137 | continue; 138 | } 139 | let value = value 140 | .clone() 141 | .expect_programming("Value should not be None after is_none check"); 142 | if value.starts_with('@') { 143 | if let Some(resolved_name) = resolve_resource_string(&value) { 144 | localized_map.insert(key.to_string(), resolved_name); 145 | } 146 | } else { 147 | localized_map.insert(key.to_string(), value.to_string()); 148 | } 149 | } 150 | } 151 | 152 | localized_map 153 | } 154 | -------------------------------------------------------------------------------- /src-tauri/src/modules/program_manager/pinyin_mapper.rs: -------------------------------------------------------------------------------- 1 | use crate::error::ResultExt; 2 | use crate::modules::config::default::PINYIN_CONTENT_JS; 3 | use serde::{Deserialize, Serialize}; 4 | /// 这个类用于将中文名字转换成拼音名字 5 | use std::collections::HashMap; 6 | 7 | #[derive(Serialize, Deserialize, Debug)] 8 | struct Item { 9 | pinyin: String, 10 | word: String, 11 | } 12 | #[derive(Debug)] 13 | pub struct PinyinMapper { 14 | pinyin: HashMap, 15 | } 16 | 17 | impl Default for PinyinMapper { 18 | fn default() -> Self { 19 | Self::new() 20 | } 21 | } 22 | 23 | impl PinyinMapper { 24 | pub fn new() -> Self { 25 | let items: Vec = serde_json::from_str(PINYIN_CONTENT_JS) 26 | .expect_programming("Failed to parse PINYIN_CONTENT_JS"); 27 | 28 | let mut word_to_pinyin: HashMap = HashMap::new(); 29 | for item in items { 30 | word_to_pinyin.insert(item.word, item.pinyin); 31 | } 32 | PinyinMapper { 33 | pinyin: word_to_pinyin, 34 | } 35 | } 36 | 37 | pub fn convert(&self, word: &str) -> String { 38 | let mut result = String::new(); 39 | let mut prev_is_han = false; // 用于跟踪前一个字符是否为 ASCII 40 | 41 | for c in word.chars() { 42 | if let Some(pinyin) = self.pinyin.get(&c.to_string()) { 43 | // 如果前一个字符是 ASCII,且结果字符串不为空,插入一个空格 44 | if !prev_is_han && !result.is_empty() { 45 | result.push(' '); 46 | } 47 | result.push_str(pinyin); 48 | result.push(' '); 49 | prev_is_han = true; // 当前字符是中文,设置标志位为 false 50 | } else { 51 | // 如果当前字符是 ASCII,直接添加 52 | result.push(c); 53 | prev_is_han = false; // 设置标志位为 true 54 | } 55 | } 56 | 57 | result.trim_end().to_string() 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src-tauri/src/modules/program_manager/search_engine.rs: -------------------------------------------------------------------------------- 1 | use crate::error::ResultExt; 2 | use crate::modules::program_manager::program_ranker::ProgramRanker; 3 | use crate::modules::program_manager::search_model::Scorer; 4 | use crate::modules::program_manager::semantic_manager::SemanticManager; 5 | use crate::program_manager::remove_repeated_space; 6 | use crate::program_manager::Program; 7 | use crate::program_manager::SearchMatchResult; 8 | use crate::program_manager::SearchModel; 9 | use crate::Arc; 10 | use rayon::prelude::*; 11 | 12 | pub(crate) trait SearchEngine: std::fmt::Debug + Send + Sync { 13 | /// 执行搜索操作 14 | /// 15 | /// # Arguments 16 | /// * `user_input` - 用户输入的搜索字符串。 17 | /// * `programs` - 可供搜索的程序列表。 18 | /// * `program_ranker` - 程序排序器实例,用于计算排序分数。 19 | /// 20 | /// # Returns 21 | /// * 一个包含搜索结果的向量,按匹配度按原始数据排列(无排序)。 22 | fn perform_search( 23 | &self, 24 | user_input: &str, 25 | programs: &[Arc], 26 | program_ranker: &ProgramRanker, 27 | ) -> Vec; 28 | } 29 | 30 | #[derive(Debug)] 31 | pub struct TraditionalSearchEngine { 32 | search_model: Arc, 33 | } 34 | 35 | impl TraditionalSearchEngine { 36 | pub fn new(search_model: Arc) -> Self { 37 | Self { search_model } 38 | } 39 | } 40 | 41 | impl Default for TraditionalSearchEngine { 42 | fn default() -> Self { 43 | Self { 44 | search_model: Arc::new(SearchModel::default()), 45 | } 46 | } 47 | } 48 | 49 | impl SearchEngine for TraditionalSearchEngine { 50 | fn perform_search( 51 | &self, 52 | user_input: &str, 53 | programs: &[Arc], 54 | program_ranker: &ProgramRanker, 55 | ) -> Vec { 56 | // 预处理用户输入 57 | let user_input = user_input.to_lowercase(); 58 | let user_input = remove_repeated_space(&user_input); 59 | 60 | let search_model = self.search_model.clone(); 61 | // 计算所有程序的匹配分数 62 | programs 63 | .par_iter() 64 | .map(|program| { 65 | // 基础匹配分数 66 | let base_score = 67 | search_model.calculate_score(program, &user_input) + program.stable_bias; 68 | 69 | // 应用智能排序增强评分 70 | let score = program_ranker.calculate_final_score( 71 | base_score, 72 | program.program_guid, 73 | &user_input, 74 | ); 75 | 76 | SearchMatchResult { 77 | score, 78 | program_guid: program.program_guid, 79 | } 80 | }) 81 | .collect() 82 | } 83 | } 84 | 85 | #[derive(Debug)] 86 | pub struct SemanticSearchEngine { 87 | semantic_model: Arc, 88 | } 89 | 90 | impl SemanticSearchEngine { 91 | pub fn new(semantic_model: Arc) -> Self { 92 | Self { semantic_model } 93 | } 94 | } 95 | 96 | impl SearchEngine for SemanticSearchEngine { 97 | fn perform_search( 98 | &self, 99 | user_input: &str, 100 | programs: &[Arc], 101 | program_ranker: &ProgramRanker, 102 | ) -> Vec { 103 | let user_input = user_input.to_lowercase(); 104 | let user_input = remove_repeated_space(&user_input); 105 | 106 | let user_embedding = self 107 | .semantic_model 108 | .generate_embedding_for_manager(&user_input) 109 | .expect_programming("Failed to generate user embedding"); 110 | 111 | // 计算所有程序的匹配分数 112 | programs 113 | .par_iter() 114 | .map(|program| { 115 | let base_score = self 116 | .semantic_model 117 | .compute_similarity(&user_embedding, &program.embedding) 118 | as f64; 119 | 120 | // 应用智能排序增强评分 121 | let score = program_ranker.calculate_final_score( 122 | base_score, 123 | program.program_guid, 124 | &user_input, 125 | ); 126 | 127 | SearchMatchResult { 128 | score, 129 | program_guid: program.program_guid, 130 | } 131 | }) 132 | .collect() 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src-tauri/src/modules/program_manager/search_model/ai_fuzzy_search_model.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src-tauri/src/modules/program_manager/search_model/launchy_search_model.rs: -------------------------------------------------------------------------------- 1 | use crate::program_manager::search_model::Scorer; 2 | use crate::program_manager::Program; 3 | /// 这个文件是以LaunchyQT的搜索模型为基础进行的改造 4 | /// 项目地址如下:https://github.com/samsonwang/LaunchyQt 5 | /// 但是launchyqt是基于比较进行搜索的,而不是基于分数的 6 | /// 所以我对这个搜索算法做了一些修改,从而可以适应当前的搜索框架 7 | use serde::{Deserialize, Serialize}; 8 | use std::collections::HashMap; 9 | use std::fmt::Debug; 10 | use std::sync::Arc; 11 | /// `LaunchyScorer` 实现了模仿 LaunchyQT 搜索算法的评分策略。 12 | /// 13 | /// 它将 Launchy 的多级比较规则(`CatLessPtr`)转化为一个数值分数, 14 | /// 以便与现有的 Scorer-based 框架集成。 15 | /// 16 | /// 评分优先级如下: 17 | /// 1. **精确匹配**: 获得最高的基础分。 18 | /// 2. **连续子串匹配**: 获得次高的基础分。 19 | /// - 匹配位置越靠前,分数越高。 20 | /// 3. **子集匹配**: 如果用户输入的字符是程序名称的子集,则获得一个较低的基础分。 21 | /// 4. **名称长度惩罚**: 作为一个微小的调整项,名称越短,分数会略微高一点,用于打破平局。 22 | /// 23 | /// 注意: Launchy 的 'usage' (使用频率) 动态权重部分没有在这里实现, 24 | /// 因为框架已在外部处理了动态分数(`program_dynamic_value_based_launch_time`)。 25 | #[derive(Serialize, Deserialize)] 26 | pub struct LaunchyScorer {} 27 | 28 | impl Debug for LaunchyScorer { 29 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 30 | f.debug_struct("LaunchyScorer").finish() 31 | } 32 | } 33 | 34 | impl Default for LaunchyScorer { 35 | fn default() -> Self { 36 | Self::new() 37 | } 38 | } 39 | 40 | impl LaunchyScorer { 41 | pub fn new() -> Self { 42 | LaunchyScorer {} 43 | } 44 | } 45 | 46 | impl Scorer for LaunchyScorer { 47 | fn calculate_score(&self, program: &Arc, user_input: &str) -> f64 { 48 | if user_input.is_empty() { 49 | // 如果没有输入,则不进行匹配,返回一个中性分数 50 | return 0.0; 51 | } 52 | 53 | let mut max_score = -1.0; // 使用一个负数作为未匹配的初始值 54 | 55 | for keyword in &program.search_keywords { 56 | let mut current_score = -1.0; 57 | 58 | // Step 1: 精确匹配 (最高优先级) 59 | if keyword.eq_ignore_ascii_case(user_input) { 60 | const EXACT_MATCH_BASE_SCORE: f64 = 100_000.0; 61 | current_score = EXACT_MATCH_BASE_SCORE; 62 | } else { 63 | // Step 2: 连续子串匹配 (次高优先级) 64 | if let Some(start_index) = keyword.to_lowercase().find(user_input) { 65 | const CONTIGUOUS_MATCH_BASE_SCORE: f64 = 10_000.0; 66 | // 匹配位置越靠前,分数越高。每个字符的偏移惩罚10分。 67 | let position_penalty = (start_index as f64) * 10.0; 68 | current_score = CONTIGUOUS_MATCH_BASE_SCORE - position_penalty; 69 | } else { 70 | let mut compare_chars = HashMap::with_capacity(keyword.len()); 71 | 72 | // 统计 compare_name 中字符出现次数 73 | for c in keyword.chars() { 74 | *compare_chars.entry(c).or_insert(0) += 1; 75 | } 76 | 77 | // 计算匹配的字符数 78 | let mut result = 0; 79 | for c in user_input.chars() { 80 | if let Some(count) = compare_chars.get_mut(&c) { 81 | if *count > 0 { 82 | result += 1; 83 | *count -= 1; 84 | } 85 | } 86 | } 87 | 88 | if result == user_input.len() { 89 | const SUBSET_MATCH_BASE_SCORE: f64 = 1_000.0; 90 | current_score = SUBSET_MATCH_BASE_SCORE; 91 | } 92 | } 93 | } 94 | 95 | if current_score > -1.0 { 96 | // Step 4: 名称长度惩罚 (用于打破平局) 97 | // 名称越长,加成越小。避免除以零。 98 | let length_bonus = 10.0 / (keyword.len() as f64 + 1.0); 99 | current_score += length_bonus; 100 | } 101 | 102 | if current_score > max_score { 103 | max_score = current_score; 104 | } 105 | } 106 | max_score 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src-tauri/src/modules/program_manager/search_model/skim_search_model.rs: -------------------------------------------------------------------------------- 1 | use crate::program_manager::search_model::Scorer; 2 | use crate::program_manager::Program; 3 | use crate::Arc; 4 | use fuzzy_matcher::skim::SkimMatcherV2; 5 | use fuzzy_matcher::FuzzyMatcher; 6 | use serde::{Deserialize, Serialize}; 7 | use std::fmt::Debug; 8 | #[derive(Serialize, Deserialize, Default)] 9 | pub struct SkimScorer { 10 | #[serde(skip)] 11 | matcher: SkimMatcherV2, 12 | } 13 | 14 | impl Scorer for SkimScorer { 15 | fn calculate_score(&self, program: &Arc, user_input: &str) -> f64 { 16 | let mut ret: f64 = -10000.0; 17 | for name in &program.search_keywords { 18 | if name.chars().count() < user_input.chars().count() { 19 | continue; 20 | } 21 | let score = self.matcher.fuzzy_match(name, user_input); 22 | if let Some(s) = score { 23 | ret = f64::max(ret, s as f64); 24 | } 25 | } 26 | ret 27 | } 28 | } 29 | 30 | impl Debug for SkimScorer { 31 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 32 | f.debug_struct("SkimScorer").finish() 33 | } 34 | } 35 | 36 | impl SkimScorer { 37 | pub fn new() -> Self { 38 | SkimScorer { 39 | matcher: SkimMatcherV2::default(), 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src-tauri/src/modules/program_manager/search_model/standard_search_model.rs: -------------------------------------------------------------------------------- 1 | use crate::program_manager::search_model::Scorer; 2 | use crate::program_manager::Program; 3 | use serde::{Deserialize, Serialize}; 4 | use std::collections::HashMap; 5 | use std::fmt::Debug; 6 | use std::sync::Arc; 7 | #[derive(Serialize, Deserialize)] 8 | pub struct StandardScorer; 9 | 10 | impl Scorer for StandardScorer { 11 | fn calculate_score(&self, program: &Arc, user_input: &str) -> f64 { 12 | // todo: 完成这个实现,如果使用到了什么子算法,用上面的模块实现出来再完成这个就可以了 13 | // program中的字符串与user_input都已经是预处理过了,不再需要预处理了 14 | let mut ret: f64 = -10000.0; 15 | for names in &program.search_keywords { 16 | if names.chars().count() < user_input.chars().count() { 17 | continue; 18 | } 19 | let mut score: f64 = shortest_edit_dis(names, user_input); 20 | score *= adjust_score_log2( 21 | (user_input.chars().count() as f64) / (names.chars().count() as f64), 22 | ); 23 | score += subset_dis(names, user_input); 24 | score += kmp(names, user_input); 25 | ret = f64::max(ret, score); 26 | } 27 | ret 28 | } 29 | } 30 | 31 | impl Debug for StandardScorer { 32 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 33 | f.debug_struct("StandardScorer").finish() 34 | } 35 | } 36 | 37 | impl Default for StandardScorer { 38 | fn default() -> Self { 39 | Self::new() 40 | } 41 | } 42 | 43 | impl StandardScorer { 44 | pub fn new() -> Self { 45 | StandardScorer 46 | } 47 | } 48 | 49 | /// 得分权重调整公式log2 50 | pub fn adjust_score_log2(origin_score: f64) -> f64 { 51 | 3.0 * ((origin_score + 1.0).log2()) 52 | } 53 | 54 | /// 子集匹配算法 55 | pub fn subset_dis(compare_name: &str, input_name: &str) -> f64 { 56 | let mut compare_chars = HashMap::with_capacity(compare_name.len()); 57 | 58 | // 统计 compare_name 中字符出现次数 59 | for c in compare_name.chars() { 60 | *compare_chars.entry(c).or_insert(0) += 1; 61 | } 62 | 63 | // 计算匹配的字符数 64 | let mut result = 0; 65 | for c in input_name.chars() { 66 | if let Some(count) = compare_chars.get_mut(&c) { 67 | if *count > 0 { 68 | result += 1; 69 | *count -= 1; 70 | } 71 | } 72 | } 73 | 74 | result as f64 75 | } 76 | 77 | /// 权重计算最短编辑距离 78 | pub fn shortest_edit_dis(compare_name: &str, input_name: &str) -> f64 { 79 | let compare_chars: Vec = compare_name.chars().collect(); 80 | let input_chars: Vec = input_name.chars().collect(); 81 | let m = compare_chars.len(); 82 | let n = input_chars.len(); 83 | 84 | if n == 0 { 85 | return 1.0; 86 | } 87 | 88 | let mut prev = vec![0i32; n + 1]; 89 | let mut current = vec![0i32; n + 1]; 90 | let mut min_operations = i32::MAX; 91 | 92 | // 初始化prev数组(对应i=0) 93 | for (j, value) in prev.iter_mut().enumerate() { 94 | *value = j as i32; 95 | } 96 | 97 | for i in 1..=m { 98 | current[0] = 0; // dp[i][0] = 0 99 | for j in 1..=n { 100 | if compare_chars[i - 1] == input_chars[j - 1] { 101 | current[j] = prev[j - 1]; 102 | } else { 103 | current[j] = std::cmp::min(prev[j - 1] + 1, prev[j] + 1); 104 | } 105 | } 106 | // 记录dp[i][n] 107 | if i >= n && current[n] < min_operations { 108 | min_operations = current[n]; 109 | } 110 | // 交换prev和current 111 | std::mem::swap(&mut prev, &mut current); 112 | } 113 | 114 | // 确保min_operations包含dp[m][n] 115 | if m >= n && prev[n] < min_operations { 116 | min_operations = prev[n]; 117 | } 118 | 119 | // 计算最终得分 120 | let value = 1.0 - (min_operations as f64 / n as f64); 121 | adjust_score_log2(n as f64) * (3.0 * value - 2.0).exp() 122 | } 123 | 124 | /// 权重计算KMP 125 | pub fn kmp(compare_name: &str, input_name: &str) -> f64 { 126 | let mut ret: f64 = 0.0; 127 | 128 | // 首字符串匹配 129 | for (c1, c2) in compare_name.chars().zip(input_name.chars()) { 130 | if c1 == c2 { 131 | ret += 1.0; 132 | } else { 133 | break; 134 | } 135 | } 136 | 137 | // 子字符串匹配 138 | if compare_name.contains(input_name) { 139 | ret += input_name.chars().count() as f64; 140 | } 141 | 142 | ret 143 | } 144 | -------------------------------------------------------------------------------- /src-tauri/src/modules/program_manager/semantic_backend.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use super::semantic_manager::EmbeddingBackend; 4 | #[cfg(feature = "ai")] 5 | use tracing::debug; 6 | 7 | #[cfg(feature = "ai")] 8 | use super::unit::{EmbeddingVec, LaunchMethod}; 9 | #[cfg(feature = "ai")] 10 | use crate::core::ai::embedding_model::embedding_gemma::EmbeddingGemmaModel; 11 | #[cfg(feature = "ai")] 12 | use crate::core::ai::embedding_model::{EmbeddingModel, EmbeddingModelType}; 13 | #[cfg(feature = "ai")] 14 | use crate::core::ai::model_manager::ModelManager; 15 | #[cfg(feature = "ai")] 16 | use ndarray::ArrayView1; 17 | #[cfg(feature = "ai")] 18 | use std::path::Path; 19 | 20 | /// 根据当前特性构建语义后端。 21 | #[cfg(feature = "ai")] 22 | pub fn create_embedding_backend( 23 | model_manager: Arc, 24 | ) -> Option> { 25 | debug!("模型管理器由外部提供,开始构建语义后端"); 26 | let backend: Arc = Arc::new(AiEmbeddingBackend::new(model_manager)); 27 | Some(backend) 28 | } 29 | 30 | /// 根据当前特性构建语义后端(无 AI 场景返回 None)。 31 | #[cfg(not(feature = "ai"))] 32 | pub fn create_embedding_backend() -> Option> { 33 | None 34 | } 35 | 36 | #[cfg(feature = "ai")] 37 | struct AiEmbeddingBackend { 38 | model_manager: Arc, 39 | } 40 | 41 | #[cfg(feature = "ai")] 42 | impl AiEmbeddingBackend { 43 | fn new(model_manager: Arc) -> Self { 44 | Self { model_manager } 45 | } 46 | } 47 | 48 | #[cfg(feature = "ai")] 49 | impl EmbeddingBackend for AiEmbeddingBackend { 50 | fn generate_embedding_for_loader( 51 | &self, 52 | show_name: &str, 53 | search_keywords: &str, 54 | launch_method: &LaunchMethod, 55 | description: &str, 56 | ) -> crate::error::AppResult { 57 | let embedding_model = self 58 | .model_manager 59 | .load_embedding_model(EmbeddingModelType::EmbeddingGemma)?; 60 | 61 | let context = format!( 62 | "软件名字:{},也叫做:{},启动地址或uwp包族名:{},描述信息:{}", 63 | show_name, 64 | search_keywords, 65 | launch_method.get_text(), 66 | description 67 | ); 68 | let combined_text = format!("title: {} | text: {}", show_name, context); 69 | 70 | let mut embedding_model_lock = embedding_model.lock(); 71 | let result = embedding_model_lock.compute_embedding(&combined_text)?; 72 | Ok(result.to_vec()) 73 | } 74 | 75 | fn generate_embedding_for_manager( 76 | &self, 77 | user_input: &str, 78 | ) -> crate::error::AppResult { 79 | let embedding_model = self 80 | .model_manager 81 | .load_embedding_model(EmbeddingModelType::EmbeddingGemma)?; 82 | 83 | let query = format!("task: search result | query: {}", user_input); 84 | let mut embedding_model_lock = embedding_model.lock(); 85 | let result = embedding_model_lock.compute_embedding(&query)?; 86 | Ok(result.to_vec()) 87 | } 88 | 89 | fn compute_similarity(&self, embedding1: &EmbeddingVec, embedding2: &EmbeddingVec) -> f32 { 90 | let view1 = ArrayView1::from(&embedding1[..]); 91 | let view2 = ArrayView1::from(&embedding2[..]); 92 | EmbeddingGemmaModel::compute_similarity(view1, view2) 93 | } 94 | 95 | fn release_resources(&self) { 96 | debug!("Releasing cached embedding model"); 97 | self.model_manager 98 | .release_embedding_model(EmbeddingModelType::EmbeddingGemma); 99 | } 100 | 101 | fn is_ready(&self) -> bool { 102 | // 粗略检测:检查默认模型文件是否存在 103 | let cfg = EmbeddingModelType::EmbeddingGemma.get_config(); 104 | Path::new(&cfg.model_path).exists() 105 | && Path::new(&cfg.tokenizer_path).exists() 106 | && Path::new(&cfg.tokenizer_config_path).exists() 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src-tauri/src/modules/program_manager/unit.rs: -------------------------------------------------------------------------------- 1 | // 存放辅助型的小类型 2 | use crate::core::image_processor::ImageIdentity; 3 | use crate::program_manager::PartialProgramManagerConfig; 4 | use bincode::{Decode, Encode}; 5 | pub type EmbeddingVec = Vec; 6 | use serde::{Deserialize, Serialize}; 7 | use std::sync::Arc; 8 | 9 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] 10 | pub enum LaunchMethodKind { 11 | Path, 12 | PackageFamilyName, 13 | File, 14 | Command, 15 | } 16 | #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Hash, Encode, Decode)] 17 | pub enum LaunchMethod { 18 | /// 通过文件路径来启动 19 | Path(String), 20 | /// 通过包族名来启动 21 | PackageFamilyName(String), 22 | /// 使用默认的启动方式来打开一个文件 23 | File(String), 24 | /// 一个自定义的命令 25 | Command(String), 26 | } 27 | 28 | impl LaunchMethod { 29 | fn template_text(&self) -> &str { 30 | match self { 31 | LaunchMethod::Path(path) => path, 32 | LaunchMethod::PackageFamilyName(name) => name, 33 | LaunchMethod::File(path) => path, 34 | LaunchMethod::Command(command) => command, 35 | } 36 | } 37 | 38 | fn map_text(&self, text: String) -> LaunchMethod { 39 | match self { 40 | LaunchMethod::Path(_) => LaunchMethod::Path(text), 41 | LaunchMethod::PackageFamilyName(_) => LaunchMethod::PackageFamilyName(text), 42 | LaunchMethod::File(_) => LaunchMethod::File(text), 43 | LaunchMethod::Command(_) => LaunchMethod::Command(text), 44 | } 45 | } 46 | 47 | /// 这个是用于在文件中存储的全局唯一标识符 48 | pub fn get_text(&self) -> String { 49 | self.template_text().to_string() 50 | } 51 | 52 | /// 统计启动模板中的"{}"占位符数量 53 | pub fn placeholder_count(&self) -> usize { 54 | self.template_text().matches("{}").count() 55 | } 56 | 57 | /// 返回启动方式的具体类型 58 | pub fn kind(&self) -> LaunchMethodKind { 59 | match self { 60 | LaunchMethod::Path(_) => LaunchMethodKind::Path, 61 | LaunchMethod::PackageFamilyName(_) => LaunchMethodKind::PackageFamilyName, 62 | LaunchMethod::File(_) => LaunchMethodKind::File, 63 | LaunchMethod::Command(_) => LaunchMethodKind::Command, 64 | } 65 | } 66 | 67 | /// 用用户输入替换模板占位符并生成新的启动方式 68 | pub fn fill_placeholders(&self, args: &[String]) -> Result { 69 | let filled = fill_template(self.template_text(), args)?; 70 | Ok(self.map_text(filled)) 71 | } 72 | 73 | pub fn is_uwp(&self) -> bool { 74 | matches!(self, LaunchMethod::PackageFamilyName(_)) 75 | } 76 | } 77 | 78 | // 根据占位符顺序依次填充模板并校验参数数量 79 | fn fill_template(template: &str, args: &[String]) -> Result { 80 | let mut result = String::with_capacity(template.len()); 81 | let mut remaining = template; 82 | let mut index = 0; 83 | 84 | while let Some(pos) = remaining.find("{}") { 85 | let (before, after_placeholder) = remaining.split_at(pos); 86 | result.push_str(before); 87 | 88 | let replacement = args.get(index).ok_or_else(|| { 89 | format!( 90 | "not enough arguments: expected at least {}, got {}", 91 | index + 1, 92 | args.len() 93 | ) 94 | })?; 95 | result.push_str(replacement); 96 | 97 | remaining = &after_placeholder[2..]; 98 | index += 1; 99 | } 100 | 101 | if index != args.len() { 102 | return Err(format!( 103 | "too many arguments: expected {}, got {}", 104 | index, 105 | args.len() 106 | )); 107 | } 108 | 109 | result.push_str(remaining); 110 | Ok(result) 111 | } 112 | 113 | /// 表示一个数据 114 | #[derive(Debug)] 115 | pub struct Program { 116 | /// 全局唯一标识符,用于快速索引,用于内存中存储 117 | pub program_guid: u64, 118 | /// 展示给用户看的名字 119 | pub show_name: String, 120 | /// 这个程序的启动方法 121 | pub launch_method: LaunchMethod, 122 | /// 用于计算的字符串 123 | pub search_keywords: Vec, 124 | /// 权重固定偏移量 125 | pub stable_bias: f64, 126 | /// 应用程序应该展示的图片的地址 127 | pub icon_path: ImageIdentity, 128 | /// 用于语义搜索的相关内容(可选) 129 | pub embedding: EmbeddingVec, 130 | } 131 | 132 | /// 表示搜索测试的结果项 133 | #[derive(Debug, Clone, Serialize, Deserialize)] 134 | pub struct SearchTestResult { 135 | /// 程序的名称 136 | pub program_name: String, 137 | /// 程序的关键字 138 | pub program_keywords: String, 139 | /// 程序的路径 140 | pub program_path: String, 141 | /// 匹配的权重值 142 | pub score: f64, 143 | } 144 | 145 | /// 表示语义信息的存储项 146 | #[derive(Debug, Serialize, Deserialize, Clone)] 147 | pub struct SemanticStoreItem { 148 | /// 程序的显示名字 149 | pub show_name: String, 150 | /// 是否为 UWP 应用 151 | pub is_uwp: bool, 152 | /// 描述信息 153 | pub description: String, 154 | } 155 | 156 | impl SemanticStoreItem { 157 | pub fn new(program: Arc) -> Self { 158 | Self { 159 | show_name: program.show_name.clone(), 160 | is_uwp: program.launch_method.is_uwp(), 161 | description: String::new(), 162 | } 163 | } 164 | } 165 | 166 | pub struct ProgramManagerRuntimeData { 167 | pub semantic_store_str: String, 168 | pub runtime_data: PartialProgramManagerConfig, 169 | pub semantic_cache_bytes: Vec, 170 | } 171 | -------------------------------------------------------------------------------- /src-tauri/src/modules/shortcut_manager/shortcut_config.rs: -------------------------------------------------------------------------------- 1 | use parking_lot::RwLock; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use super::Shortcut; 5 | 6 | #[derive(Serialize, Deserialize, Debug, Clone, Default)] 7 | pub struct PartialShortcutConfig { 8 | pub open_search_bar: Option, 9 | pub arrow_up: Option, 10 | pub arrow_down: Option, 11 | pub arrow_left: Option, 12 | pub arrow_right: Option, 13 | } 14 | 15 | /// 快捷键配置 16 | #[derive(Serialize, Deserialize, Debug, Clone)] 17 | #[serde(default)] 18 | pub struct ShortcutConfigInner { 19 | #[serde(default = "ShortcutConfigInner::default_open_search_bar")] 20 | pub open_search_bar: Shortcut, 21 | #[serde(default = "ShortcutConfigInner::default_arrow_up")] 22 | pub arrow_up: Shortcut, 23 | #[serde(default = "ShortcutConfigInner::default_arrow_down")] 24 | pub arrow_down: Shortcut, 25 | #[serde(default = "ShortcutConfigInner::default_arrow_left")] 26 | pub arrow_left: Shortcut, 27 | #[serde(default = "ShortcutConfigInner::default_arrow_right")] 28 | pub arrow_right: Shortcut, 29 | } 30 | 31 | impl Default for ShortcutConfigInner { 32 | fn default() -> Self { 33 | Self { 34 | open_search_bar: Self::default_open_search_bar(), 35 | arrow_up: Self::default_arrow_up(), 36 | arrow_down: Self::default_arrow_down(), 37 | arrow_left: Self::default_arrow_left(), 38 | arrow_right: Self::default_arrow_right(), 39 | } 40 | } 41 | } 42 | 43 | impl ShortcutConfigInner { 44 | pub(crate) fn default_open_search_bar() -> Shortcut { 45 | let mut shortcut = Shortcut::new(); 46 | shortcut.key = "Space".to_string(); 47 | shortcut.alt = true; 48 | shortcut 49 | } 50 | 51 | pub(crate) fn default_arrow_up() -> Shortcut { 52 | let mut shortcut = Shortcut::new(); 53 | shortcut.key = "k".to_string(); 54 | shortcut.ctrl = true; 55 | shortcut 56 | } 57 | 58 | pub(crate) fn default_arrow_down() -> Shortcut { 59 | let mut shortcut = Shortcut::new(); 60 | shortcut.key = "j".to_string(); 61 | shortcut.ctrl = true; 62 | shortcut 63 | } 64 | 65 | pub(crate) fn default_arrow_left() -> Shortcut { 66 | let mut shortcut = Shortcut::new(); 67 | shortcut.key = "h".to_string(); 68 | shortcut.ctrl = true; 69 | shortcut 70 | } 71 | 72 | pub(crate) fn default_arrow_right() -> Shortcut { 73 | let mut shortcut = Shortcut::new(); 74 | shortcut.key = "l".to_string(); 75 | shortcut.ctrl = true; 76 | shortcut 77 | } 78 | 79 | pub fn update(&mut self, partial: PartialShortcutConfig) { 80 | if let Some(shortcut) = partial.open_search_bar { 81 | self.open_search_bar = shortcut; 82 | } 83 | if let Some(shortcut) = partial.arrow_up { 84 | self.arrow_up = shortcut; 85 | } 86 | if let Some(shortcut) = partial.arrow_down { 87 | self.arrow_down = shortcut; 88 | } 89 | if let Some(shortcut) = partial.arrow_left { 90 | self.arrow_left = shortcut; 91 | } 92 | if let Some(shortcut) = partial.arrow_right { 93 | self.arrow_right = shortcut; 94 | } 95 | } 96 | 97 | pub fn to_partial(&self) -> PartialShortcutConfig { 98 | PartialShortcutConfig { 99 | open_search_bar: Some(self.open_search_bar.clone()), 100 | arrow_up: Some(self.arrow_up.clone()), 101 | arrow_down: Some(self.arrow_down.clone()), 102 | arrow_left: Some(self.arrow_left.clone()), 103 | arrow_right: Some(self.arrow_right.clone()), 104 | } 105 | } 106 | } 107 | 108 | #[derive(Debug)] 109 | pub struct ShortcutConfig { 110 | inner: RwLock, 111 | } 112 | 113 | impl Default for ShortcutConfig { 114 | fn default() -> Self { 115 | ShortcutConfig { 116 | inner: RwLock::new(ShortcutConfigInner::default()), 117 | } 118 | } 119 | } 120 | 121 | impl ShortcutConfig { 122 | pub fn update(&self, partial: PartialShortcutConfig) { 123 | let mut inner = self.inner.write(); 124 | inner.update(partial); 125 | } 126 | 127 | pub fn get_open_search_bar(&self) -> Shortcut { 128 | let inner = self.inner.read(); 129 | inner.open_search_bar.clone() 130 | } 131 | 132 | pub fn get_arrow_up(&self) -> Shortcut { 133 | let inner = self.inner.read(); 134 | inner.arrow_up.clone() 135 | } 136 | 137 | pub fn get_arrow_down(&self) -> Shortcut { 138 | let inner = self.inner.read(); 139 | inner.arrow_down.clone() 140 | } 141 | 142 | pub fn get_arrow_left(&self) -> Shortcut { 143 | let inner = self.inner.read(); 144 | inner.arrow_left.clone() 145 | } 146 | 147 | pub fn get_arrow_right(&self) -> Shortcut { 148 | let inner = self.inner.read(); 149 | inner.arrow_right.clone() 150 | } 151 | 152 | pub fn to_partial(&self) -> PartialShortcutConfig { 153 | let inner = self.inner.read(); 154 | inner.to_partial() 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src-tauri/src/modules/ui_controller/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod controller; 2 | -------------------------------------------------------------------------------- /src-tauri/src/modules/version_checker/mod.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use reqwest::Client; 3 | use serde::Deserialize; 4 | 5 | #[derive(Debug, Deserialize)] 6 | struct GitHubRelease { 7 | tag_name: String, 8 | } 9 | 10 | pub struct VersionChecker {} 11 | 12 | impl VersionChecker { 13 | /// 获得当前软件最新的版本 14 | pub async fn get_latest_release_version() -> Result { 15 | // 硬编码仓库信息 16 | const OWNER: &str = "ghost-him"; 17 | const REPO: &str = "ZeroLaunch-rs"; 18 | let client = Client::new(); 19 | let url = format!( 20 | "https://api.github.com/repos/{}/{}/releases/latest", 21 | OWNER, REPO 22 | ); 23 | let response = client 24 | .get(&url) 25 | .header("User-Agent", "ZeroLaunch-rs-Version-Checker") 26 | .send() 27 | .await?; 28 | if !response.status().is_success() { 29 | return Err(anyhow!( 30 | "Failed to fetch GitHub release: HTTP {}", 31 | response.status() 32 | )); 33 | } 34 | let release: GitHubRelease = response.json().await?; 35 | Ok(release.tag_name) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src-tauri/src/state/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod app_state; 2 | -------------------------------------------------------------------------------- /src-tauri/src/utils/access_policy.rs: -------------------------------------------------------------------------------- 1 | pub trait StateAccess { 2 | type Target; 3 | 4 | // 安全访问方法 5 | fn with_state(&self, f: F) -> R 6 | where 7 | F: FnOnce(&Self::Target) -> R; 8 | } 9 | -------------------------------------------------------------------------------- /src-tauri/src/utils/defer.rs: -------------------------------------------------------------------------------- 1 | /// 实现类似于go语言中的defer函数 2 | use std::ops::Drop; 3 | 4 | pub struct Defer { 5 | f: Option, 6 | } 7 | 8 | impl Defer { 9 | fn new(f: F) -> Self { 10 | Defer { f: Some(f) } 11 | } 12 | } 13 | 14 | impl Drop for Defer { 15 | fn drop(&mut self) { 16 | if let Some(f) = self.f.take() { 17 | f(); 18 | } 19 | } 20 | } 21 | 22 | pub fn defer(f: F) -> Defer { 23 | Defer::new(f) 24 | } 25 | -------------------------------------------------------------------------------- /src-tauri/src/utils/font_database.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use fontdb::Database; 4 | 5 | /// 返回当前系统上安装的所有的字体 6 | pub fn get_fonts() -> HashSet { 7 | let mut result = HashSet::new(); 8 | let mut db = Database::new(); 9 | db.load_system_fonts(); 10 | 11 | for face in db.faces() { 12 | let has_chinese = face 13 | .families 14 | .iter() 15 | .any(|(_family, language)| language.primary_language() == "Chinese"); 16 | 17 | if has_chinese { 18 | // 找到支持中文的字体名称 19 | if let Some((chinese_name, _)) = face 20 | .families 21 | .iter() 22 | .find(|(_, language)| language.primary_language() == "Chinese") 23 | { 24 | result.insert(chinese_name.clone()); 25 | } 26 | } else { 27 | // 如果没有中文支持,使用默认名称(第一个家族名称) 28 | if !face.families.is_empty() { 29 | result.insert(face.families[0].0.clone()); 30 | } 31 | } 32 | } 33 | result 34 | } 35 | -------------------------------------------------------------------------------- /src-tauri/src/utils/locale.rs: -------------------------------------------------------------------------------- 1 | /// 系统区域设置和语言检测工具 2 | use tracing::{debug, info, warn}; 3 | use windows::Win32::Globalization::GetUserDefaultLocaleName; 4 | 5 | /// 使用 Windows API GetUserDefaultLocaleName 来获取用户的默认区域设置 6 | pub fn get_system_locale() -> Option { 7 | unsafe { 8 | const LOCALE_NAME_MAX_LENGTH: usize = 85; 9 | let mut locale_name: [u16; LOCALE_NAME_MAX_LENGTH] = [0; LOCALE_NAME_MAX_LENGTH]; 10 | 11 | let result = GetUserDefaultLocaleName(&mut locale_name); 12 | 13 | if result > 0 { 14 | // 找到第一个 null 终止符 15 | let len = locale_name 16 | .iter() 17 | .position(|&c| c == 0) 18 | .unwrap_or(result as usize); 19 | let locale_string = String::from_utf16_lossy(&locale_name[..len]); 20 | debug!("检测到系统语言: {}", locale_string); 21 | Some(locale_string) 22 | } else { 23 | warn!("无法获取系统语言设置"); 24 | None 25 | } 26 | } 27 | } 28 | 29 | pub fn map_locale_to_language(locale: &str) -> String { 30 | // 转换为小写以便于匹配 31 | let locale_lower = locale.to_lowercase(); 32 | 33 | if locale_lower.starts_with("zh-") { 34 | let traditional_locales = ["zh-tw", "zh-hk", "zh-mo", "zh-hant"]; 35 | 36 | for traditional in &traditional_locales { 37 | if locale_lower.starts_with(traditional) { 38 | return "zh-Hant".to_string(); 39 | } 40 | } 41 | 42 | // 默认其他中文 locale 为简体中文 43 | return "zh-Hans".to_string(); 44 | } 45 | 46 | // 英语处理 47 | if locale_lower.starts_with("en-") || locale_lower == "en" { 48 | debug!("系统语言 {} 映射为英语", locale); 49 | return "en".to_string(); 50 | } 51 | 52 | "en".to_string() 53 | } 54 | 55 | /// 获取适合应用的默认语言 56 | /// 57 | /// 尝试检测系统语言并映射到应用支持的语言,如果检测失败则返回英语 58 | pub fn get_default_app_language() -> String { 59 | match get_system_locale() { 60 | Some(locale) => map_locale_to_language(&locale), 61 | None => { 62 | info!("无法检测系统语言,使用英语作为默认"); 63 | "en".to_string() 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src-tauri/src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod access_policy; 2 | pub mod defer; 3 | pub mod font_database; 4 | pub mod i18n; 5 | pub mod locale; 6 | pub mod notify; 7 | pub mod service_locator; 8 | pub mod ui_controller; 9 | pub mod waiting_hashmap; 10 | pub mod windows; 11 | use chrono::{Local, NaiveDate}; 12 | use dashmap::DashMap; 13 | use std::collections::HashMap; 14 | use std::hash::Hash; 15 | use time::OffsetDateTime; 16 | /// 生成当前日期的函数 17 | pub fn generate_current_date() -> String { 18 | let current_date = Local::now().date_naive(); 19 | current_date.format("%Y-%m-%d").to_string() 20 | } 21 | 22 | /// 生成当前的时间 23 | pub fn get_current_time() -> i64 { 24 | let now = OffsetDateTime::now_utc(); 25 | now.unix_timestamp() 26 | } 27 | 28 | /// 比较日期字符串与当前日期的函数 29 | pub fn is_date_current(date_str: &str) -> bool { 30 | // 解析输入的日期字符串 31 | let input_date = match NaiveDate::parse_from_str(date_str, "%Y-%m-%d") { 32 | Ok(date) => date, 33 | Err(e) => { 34 | tracing::warn!("Failed to parse date string '{}': {}", date_str, e); 35 | return false; // 如果解析失败,返回false 36 | } 37 | }; 38 | 39 | // 获取当前日期 40 | let current_date = Local::now().date_naive(); 41 | 42 | // 比较两个日期 43 | input_date == current_date 44 | } 45 | 46 | // 将 DashMap 转换为 HashMap 47 | pub fn dashmap_to_hashmap(dash_map: &DashMap) -> HashMap 48 | where 49 | K: Hash + Eq + Clone, 50 | V: Clone, 51 | { 52 | dash_map 53 | .iter() 54 | .map(|r| (r.key().clone(), r.value().clone())) 55 | .collect() 56 | } 57 | 58 | // 将 HashMap 转换为 DashMap 59 | pub fn hashmap_to_dashmap(hash_map: &HashMap) -> DashMap 60 | where 61 | K: Hash + Eq + Clone, 62 | V: Clone, 63 | { 64 | let dash_map = DashMap::with_capacity(hash_map.len()); 65 | for (key, value) in hash_map { 66 | dash_map.insert(key.clone(), value.clone()); 67 | } 68 | dash_map 69 | } 70 | -------------------------------------------------------------------------------- /src-tauri/src/utils/notify.rs: -------------------------------------------------------------------------------- 1 | use super::i18n::{t, t_with}; 2 | use super::service_locator::ServiceLocator; 3 | use tauri_plugin_notification::NotificationExt; 4 | 5 | pub fn notify(title: &str, message: &str) { 6 | let state = ServiceLocator::get_state(); 7 | let app_handle = state.get_main_handle(); 8 | 9 | if let Err(e) = app_handle 10 | .notification() 11 | .builder() 12 | .title(title) 13 | .body(message) 14 | .show() 15 | { 16 | tracing::error!("Failed to show notification: {}", e); 17 | } 18 | } 19 | 20 | /// 国际化通知函数 21 | /// 22 | /// 使用翻译键显示通知 23 | /// 24 | /// # 参数 25 | /// * `title` - 通知标题(不翻译) 26 | /// * `message_key` - 通知消息的翻译键 27 | /// 28 | pub fn notify_i18n(title: &str, message_key: &str) { 29 | let message = t(message_key); 30 | notify(title, &message); 31 | } 32 | 33 | /// 国际化通知函数(带占位符替换) 34 | /// 35 | /// 使用翻译键显示通知,并替换占位符 36 | /// 37 | /// # 参数 38 | /// * `title` - 通知标题(不翻译) 39 | /// * `message_key` - 通知消息的翻译键 40 | /// * `replacements` - 占位符替换数组 41 | /// 42 | pub fn notify_i18n_with(title: &str, message_key: &str, replacements: &[(&str, &str)]) { 43 | let message = t_with(message_key, replacements); 44 | notify(title, &message); 45 | } 46 | -------------------------------------------------------------------------------- /src-tauri/src/utils/service_locator.rs: -------------------------------------------------------------------------------- 1 | // src-tauri/src/infrastructure/service_locator.rs 2 | use super::super::error::{OptionExt, ResultExt}; 3 | use super::super::state::app_state::AppState; 4 | use std::sync::Arc; 5 | use std::sync::OnceLock; 6 | 7 | static APP_STATE: OnceLock> = OnceLock::new(); 8 | 9 | pub struct ServiceLocator; 10 | 11 | impl ServiceLocator { 12 | pub fn init(state: Arc) { 13 | APP_STATE 14 | .set(state) 15 | .expect_programming("Failed to initialize app state"); 16 | } 17 | 18 | pub fn get_state() -> Arc { 19 | APP_STATE 20 | .get() 21 | .cloned() 22 | .expect_programming("State not initialized") 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src-tauri/src/utils/ui_controller.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | use tauri::webview::WebviewWindow; 3 | use tauri::Emitter; 4 | use tauri::Manager; 5 | use tracing::warn; 6 | 7 | use super::service_locator::ServiceLocator; 8 | use super::windows::is_foreground_fullscreen; 9 | use crate::update_window_size_and_position; 10 | 11 | pub fn handle_pressed(app_handle: &tauri::AppHandle) { 12 | // 如果不是全屏情况下才唤醒 13 | let state = ServiceLocator::get_state(); 14 | let runtime_config = state.get_runtime_config(); 15 | let app_config = runtime_config.get_app_config(); 16 | 17 | if !app_config.get_is_wake_on_fullscreen() && is_foreground_fullscreen() { 18 | return; 19 | } 20 | 21 | update_window_size_and_position(); 22 | 23 | let main_window = match app_handle.get_webview_window("main") { 24 | Some(window) => Arc::new(window), 25 | None => { 26 | warn!("Failed to get main window"); 27 | return; 28 | } 29 | }; 30 | 31 | if let Err(e) = main_window.show() { 32 | warn!("Failed to show main window: {}", e); 33 | return; 34 | } 35 | 36 | if let Err(e) = main_window.set_focus() { 37 | warn!("Failed to set focus on main window: {}", e); 38 | return; 39 | } 40 | 41 | if let Err(e) = main_window.emit("show_window", ()) { 42 | warn!("Failed to emit show_window event: {}", e); 43 | } 44 | let state = ServiceLocator::get_state(); 45 | state.set_search_bar_visible(true); 46 | } 47 | 48 | pub fn handle_focus_lost(main_window: Arc) { 49 | main_window 50 | .hide() 51 | .unwrap_or_else(|e| warn!("无法隐藏窗口:{}", e)); 52 | if let Err(e) = main_window.emit("handle_focus_lost", ()) { 53 | warn!("Failed to emit handle_focus_lost event: {}", e); 54 | } 55 | let state = ServiceLocator::get_state(); 56 | state.set_search_bar_visible(false); 57 | } 58 | -------------------------------------------------------------------------------- /src-tauri/src/utils/waiting_hashmap.rs: -------------------------------------------------------------------------------- 1 | use std::collections::hash_map::Entry; 2 | use std::collections::HashMap; 3 | use std::hash::Hash; 4 | use std::sync::Arc; 5 | use tokio::sync::{Mutex, Notify}; 6 | 7 | /// 一个支持异步等待的 HashMap 8 | #[derive(Debug)] 9 | pub struct AsyncWaitingHashMap { 10 | data: Arc>>, 11 | notifiers: Arc>>>, 12 | } 13 | 14 | impl AsyncWaitingHashMap 15 | where 16 | K: Eq + Hash + Clone + Send + Sync + 'static, 17 | V: Clone + Send + Sync + 'static, 18 | { 19 | /// 创建一个新的 AsyncWaitingHashMap 20 | pub fn new() -> Self { 21 | AsyncWaitingHashMap { 22 | data: Arc::new(Mutex::new(HashMap::new())), 23 | notifiers: Arc::new(Mutex::new(HashMap::new())), 24 | } 25 | } 26 | 27 | /// 异步插入键值对,如果有等待该键的任务,则通知它们 28 | pub async fn insert(&self, key: K, value: V) -> Option { 29 | let mut data = self.data.lock().await; 30 | let result = data.insert(key.clone(), value); 31 | 32 | // 通知所有等待这个键的任务 33 | let notifiers = self.notifiers.lock().await; 34 | if let Some(notifier) = notifiers.get(&key) { 35 | notifier.notify_waiters(); 36 | } 37 | 38 | result 39 | } 40 | 41 | /// 异步获取键对应的值,如果不存在则等待 42 | pub async fn get_or_wait(&self, key: K) -> V { 43 | // 首先尝试获取值 44 | { 45 | let data = self.data.lock().await; 46 | if let Some(value) = data.get(&key) { 47 | return value.clone(); 48 | } 49 | } 50 | 51 | // 如果值不存在,获取或创建一个通知器 52 | let notifier = { 53 | let mut notifiers = self.notifiers.lock().await; 54 | match notifiers.entry(key.clone()) { 55 | Entry::Occupied(entry) => entry.get().clone(), 56 | Entry::Vacant(entry) => { 57 | let notify = Arc::new(Notify::new()); 58 | entry.insert(notify.clone()); 59 | notify 60 | } 61 | } 62 | }; 63 | 64 | // 循环等待值出现 65 | loop { 66 | // 再次检查值是否已经存在 67 | { 68 | let data = self.data.lock().await; 69 | if let Some(value) = data.get(&key) { 70 | return value.clone(); 71 | } 72 | } 73 | 74 | // 等待通知 75 | notifier.notified().await; 76 | 77 | // 被通知后再次检查值 78 | let data = self.data.lock().await; 79 | if let Some(value) = data.get(&key) { 80 | // 清理不再需要的通知器 81 | let mut notifiers = self.notifiers.lock().await; 82 | notifiers.remove(&key); 83 | 84 | return value.clone(); 85 | } 86 | } 87 | } 88 | 89 | /// 异步尝试获取值,但不等待(返回 Option) 90 | pub async fn get(&self, key: &K) -> Option { 91 | let data = self.data.lock().await; 92 | data.get(key).cloned() 93 | } 94 | 95 | /// 异步删除键值对 96 | pub async fn remove(&self, key: &K) -> Option { 97 | let mut data = self.data.lock().await; 98 | data.remove(key) 99 | } 100 | 101 | /// 异步检查键是否存在 102 | pub async fn contains_key(&self, key: &K) -> bool { 103 | let data = self.data.lock().await; 104 | data.contains_key(key) 105 | } 106 | 107 | /// 异步获取当前 HashMap 中的键值对数量 108 | pub async fn len(&self) -> usize { 109 | let data = self.data.lock().await; 110 | data.len() 111 | } 112 | 113 | /// 异步检查 HashMap 是否为空 114 | pub async fn is_empty(&self) -> bool { 115 | let data = self.data.lock().await; 116 | data.is_empty() 117 | } 118 | 119 | /// 异步清空 HashMap 120 | pub async fn clear(&self) { 121 | let mut data = self.data.lock().await; 122 | data.clear(); 123 | } 124 | } 125 | 126 | impl Default for AsyncWaitingHashMap 127 | where 128 | K: Eq + Hash + Clone + Send + Sync + 'static, 129 | V: Clone + Send + Sync + 'static, 130 | { 131 | fn default() -> Self { 132 | Self::new() 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src-tauri/src/utils/windows.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::OsStr; 2 | use std::os::windows::ffi::OsStrExt; 3 | /// 存放与windows相关的工具类函数 4 | use std::path::Path; 5 | use windows::core::PCWSTR; 6 | use windows::Win32::Foundation::{GetLastError, HWND, POINT, RECT, WIN32_ERROR}; 7 | use windows::Win32::Graphics::Gdi::{ 8 | GetMonitorInfoW, MonitorFromWindow, MONITORINFO, MONITOR_DEFAULTTOPRIMARY, 9 | }; 10 | use windows::Win32::System::Environment::ExpandEnvironmentStringsW; 11 | use windows::Win32::UI::Shell::ShellExecuteW; 12 | use windows::Win32::UI::WindowsAndMessaging::{ 13 | GetForegroundWindow, GetParent, GetWindowRect, WindowFromPoint, SW_SHOWNORMAL, 14 | }; 15 | /// 将一个字符串转成windows的宽字符 16 | pub fn get_u16_vec>(path: P) -> Vec { 17 | path.as_ref() 18 | .as_os_str() 19 | .encode_wide() 20 | .chain(std::iter::once(0)) 21 | .collect() 22 | } 23 | /// 检测当前前台窗口是否处于全屏状态 24 | pub fn is_foreground_fullscreen() -> bool { 25 | unsafe { 26 | // 获取当前前台窗口句柄 27 | let foreground_hwnd = GetForegroundWindow(); 28 | if foreground_hwnd.0.is_null() { 29 | return false; 30 | } 31 | 32 | // 获取主显示器信息 33 | let monitor = MonitorFromWindow(foreground_hwnd, MONITOR_DEFAULTTOPRIMARY); 34 | let mut monitor_info = MONITORINFO { 35 | cbSize: std::mem::size_of::() as u32, 36 | ..Default::default() 37 | }; 38 | 39 | if !GetMonitorInfoW(monitor, &mut monitor_info).as_bool() { 40 | return false; 41 | } 42 | 43 | let screen_rect = monitor_info.rcMonitor; 44 | let screen_width = screen_rect.right - screen_rect.left; 45 | let screen_height = screen_rect.bottom - screen_rect.top; 46 | 47 | // 获取左下角坐标所属的顶层窗口 48 | let left_bottom_point = POINT { 49 | x: screen_rect.left, 50 | y: screen_rect.bottom - 1, 51 | }; 52 | 53 | let left_bottom_hwnd = top_window_from_point(left_bottom_point); 54 | 55 | // 如果前台窗口与左下角窗口不同,则不是全屏 56 | if foreground_hwnd.0 != left_bottom_hwnd.0 { 57 | return false; 58 | } 59 | 60 | // 获取前台窗口的尺寸 61 | let mut window_rect = RECT::default(); 62 | if GetWindowRect(foreground_hwnd, &mut window_rect).is_err() { 63 | return false; 64 | } 65 | 66 | // 检查窗口尺寸是否与屏幕尺寸相当 67 | let window_width = window_rect.right - window_rect.left; 68 | let window_height = window_rect.bottom - window_rect.top; 69 | 70 | window_width >= screen_width && window_height >= screen_height 71 | } 72 | } 73 | 74 | /// 获取指定坐标点所属的顶层窗口 75 | fn top_window_from_point(point: POINT) -> HWND { 76 | unsafe { 77 | let mut hwnd = WindowFromPoint(point); 78 | 79 | // 循环获取父窗口,直到找到顶层窗口 80 | while let Ok(parent) = GetParent(hwnd) { 81 | if parent.0.is_null() { 82 | break; 83 | } 84 | hwnd = parent; 85 | } 86 | 87 | hwnd 88 | } 89 | } 90 | 91 | /// 使用 Windows API 展开环境变量 92 | pub fn expand_environment_variables(input: &str) -> Option { 93 | unsafe { 94 | // 转换为 UTF-16 95 | let wide_input: Vec = OsStr::new(input) 96 | .encode_wide() 97 | .chain(std::iter::once(0)) 98 | .collect(); 99 | 100 | // 首先获取需要的缓冲区大小 101 | let required_size = ExpandEnvironmentStringsW(PCWSTR::from_raw(wide_input.as_ptr()), None); 102 | 103 | if required_size == 0 { 104 | return None; 105 | } 106 | 107 | // 分配缓冲区并展开 108 | let mut buffer: Vec = vec![0; required_size as usize]; 109 | let result = 110 | ExpandEnvironmentStringsW(PCWSTR::from_raw(wide_input.as_ptr()), Some(&mut buffer)); 111 | 112 | if result > 0 && result <= required_size { 113 | // 移除末尾的 null 终止符 114 | if let Some(&0) = buffer.last() { 115 | buffer.pop(); 116 | } 117 | Some(String::from_utf16_lossy(&buffer)) 118 | } else { 119 | None 120 | } 121 | } 122 | } 123 | 124 | /// 使用 ShellExecuteW 以系统默认方式打开指定路径 125 | pub fn shell_execute_open>(path: P) -> Result<(), WIN32_ERROR> { 126 | let wide_path = get_u16_vec(path); 127 | 128 | unsafe { 129 | let result = ShellExecuteW( 130 | None, 131 | PCWSTR::from_raw(std::ptr::null()), 132 | PCWSTR::from_raw(wide_path.as_ptr()), 133 | PCWSTR::from_raw(std::ptr::null()), 134 | PCWSTR::from_raw(std::ptr::null()), 135 | SW_SHOWNORMAL, 136 | ); 137 | 138 | if result.0 as isize <= 32 { 139 | Err(GetLastError()) 140 | } else { 141 | Ok(()) 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src-tauri/src/window_effect.rs: -------------------------------------------------------------------------------- 1 | use tauri::window::EffectsBuilder; 2 | use tauri::Manager; 3 | 4 | use crate::error::OptionExt; 5 | use crate::utils::service_locator::ServiceLocator; 6 | use windows::{ 7 | core::*, 8 | Win32::Graphics::Dwm::{ 9 | DwmSetWindowAttribute, DWMWA_WINDOW_CORNER_PREFERENCE, DWMWINDOWATTRIBUTE, 10 | }, 11 | }; 12 | 13 | // 更新窗口的状态 14 | pub fn enable_window_effect() { 15 | // 1. 更新是不是毛玻璃的效果 16 | update_blur_effect(); 17 | // 2. 更新圆角的大小 18 | if let Err(e) = update_rounded_corners() { 19 | println!("{:?}", e); 20 | } 21 | } 22 | 23 | pub fn update_blur_effect() { 24 | let state = ServiceLocator::get_state(); 25 | let handle = state.get_main_handle(); 26 | let main_window = handle 27 | .get_webview_window("main") 28 | .expect_programming("无法获取主窗口"); 29 | let runtime_config = state.get_runtime_config(); 30 | let ui_config = runtime_config.get_ui_config(); 31 | let blur_style = ui_config.get_blur_style(); 32 | let mut builder = EffectsBuilder::new(); 33 | 34 | if ui_config.get_use_windows_sys_control_radius() { 35 | if let Some(effect) = match blur_style.as_str() { 36 | "Acrylic" => Some(tauri_utils::WindowEffect::Acrylic), 37 | "Mica" => Some(tauri_utils::WindowEffect::Mica), 38 | "Tabbed" => Some(tauri_utils::WindowEffect::Tabbed), 39 | _ => None, 40 | } { 41 | builder = builder.effect(effect); 42 | } 43 | } 44 | 45 | let effects = builder.build(); 46 | 47 | if ui_config.get_use_windows_sys_control_radius() { 48 | let _ = main_window.set_effects(effects); 49 | } else { 50 | let _ = main_window.set_effects(None); 51 | } 52 | } 53 | 54 | // 定义圆角类型常量(Windows 11) 55 | const DWMWCP_ROUND: u32 = 2; 56 | const DWMWCP_DONOTROUND: u32 = 1; 57 | 58 | /// 更新窗口圆角设置 59 | pub fn update_rounded_corners() -> Result<()> { 60 | let state = ServiceLocator::get_state(); 61 | let handle = state.get_main_handle(); 62 | let main_window = handle 63 | .get_webview_window("main") 64 | .expect_programming("无法获取主窗口"); 65 | let hwnd = main_window.hwnd().expect("无法获取窗口句柄"); 66 | let use_windows_sys_control_radius = state 67 | .get_runtime_config() 68 | .get_ui_config() 69 | .get_use_windows_sys_control_radius(); 70 | unsafe { 71 | // 设置窗口圆角 72 | let corner_preference = if use_windows_sys_control_radius { 73 | DWMWCP_ROUND 74 | } else { 75 | // 不使用圆角 76 | DWMWCP_DONOTROUND 77 | }; 78 | DwmSetWindowAttribute( 79 | hwnd, 80 | DWMWINDOWATTRIBUTE(DWMWA_WINDOW_CORNER_PREFERENCE.0), 81 | &corner_preference as *const _ as *const _, 82 | std::mem::size_of::() as u32, 83 | ) 84 | .ok(); 85 | } 86 | Ok(()) 87 | } 88 | -------------------------------------------------------------------------------- /src-tauri/tauri.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.tauri.app/config/2", 3 | "productName": "zerolaunch-rs", 4 | "version": "0.5.2", 5 | "identifier": "com.zerolaunch-rs.app", 6 | "build": { 7 | "beforeDevCommand": "bun run dev", 8 | "devUrl": "http://localhost:12345", 9 | "beforeBuildCommand": "bun run build", 10 | "frontendDist": "../dist" 11 | }, 12 | "app": { 13 | "windows": [ 14 | { 15 | "title": "zerolaunch-rs", 16 | "fullscreen": false, 17 | "resizable": false, 18 | "decorations": false, 19 | "transparent": true, 20 | "shadow": false, 21 | "alwaysOnTop": true, 22 | "dragDropEnabled": true, 23 | "skipTaskbar": true 24 | } 25 | ], 26 | "security": { 27 | "csp": null 28 | } 29 | }, 30 | "bundle": { 31 | "active": true, 32 | "targets": "all", 33 | "resources":["icons/", "locales/", "models/readme.md"], 34 | "icon": [ 35 | "icons/32x32.png", 36 | "icons/128x128.png", 37 | "icons/128x128@2x.png", 38 | "icons/icon.icns", 39 | "icons/icon.ico", 40 | "icons/web_pages.png", 41 | "icons/tips.png", 42 | "icons/terminal.png" 43 | ] 44 | }, 45 | "plugins": { 46 | "deep-link": { 47 | "desktop": { 48 | "schemes": ["zerolaunch-rs"] 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src-tauri/tauri.conf.portable.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.tauri.app/config/2", 3 | "app": { 4 | "security": { 5 | "csp": null 6 | }, 7 | "windows": [ 8 | { 9 | "alwaysOnTop": true, 10 | "decorations": false, 11 | "dragDropEnabled": true, 12 | "fullscreen": false, 13 | "resizable": false, 14 | "shadow": false, 15 | "skipTaskbar": true, 16 | "title": "zerolaunch-rs_portable", 17 | "transparent": true 18 | } 19 | ] 20 | }, 21 | "build": { 22 | "beforeBuildCommand": "bun run build", 23 | "beforeDevCommand": "bun run dev", 24 | "devUrl": "http://localhost:12345", 25 | "frontendDist": "../dist" 26 | }, 27 | "bundle": { 28 | "active": false, 29 | "icon": [ 30 | "icons/32x32.png", 31 | "icons/128x128.png", 32 | "icons/128x128@2x.png", 33 | "icons/icon.icns", 34 | "icons/icon.ico", 35 | "icons/web_pages.png", 36 | "icons/tips.png", 37 | "icons/terminal.png" 38 | ], 39 | "resources": [ 40 | "icons/", 41 | "locales/", 42 | "models/readme.md" 43 | ], 44 | "targets": "all" 45 | }, 46 | "identifier": "com.zerolaunch-rs.portable.app", 47 | "plugins": { 48 | "deep-link": { 49 | "desktop": { 50 | "schemes": [ 51 | "zerolaunch-rs_portable" 52 | ] 53 | } 54 | } 55 | }, 56 | "productName": "zerolaunch-rs_portable", 57 | "version": "0.5.2" 58 | } -------------------------------------------------------------------------------- /src/api/local_config_types.ts: -------------------------------------------------------------------------------- 1 | /** 存储目的地枚举 */ 2 | export type StorageDestination = "WebDAV" | "Local" | "OneDrive"; 3 | 4 | export type LocalConfig = { 5 | storage_destination: StorageDestination; 6 | local_save_config: LocalSaveConfig; 7 | webdav_save_config: WebDAVConfig; 8 | // onedrive_save_config: OneDriveConfig; 9 | save_to_local_per_update: number; 10 | welcome_page_version: string; 11 | } 12 | 13 | export type LocalSaveConfig = { 14 | destination_dir: string; 15 | } 16 | 17 | export type WebDAVConfig = { 18 | host_url: string; 19 | account: string; 20 | password: string; 21 | destination_dir: string; 22 | } 23 | 24 | // export type OneDriveConfig = { 25 | // refresh_token: string; 26 | // destination_dir: string; 27 | // } 28 | 29 | export type LocalStorageInner = { 30 | remote_config_dir: string; 31 | } 32 | 33 | export type PartialLocalSaveConfig = Partial 34 | 35 | export type PartialWebDAVConfig = Partial 36 | 37 | // export type PartialOneDriveConfig = Partial 38 | 39 | export type PartialLocalConfig = { 40 | storage_destination?: StorageDestination; 41 | local_save_config?: PartialLocalSaveConfig; 42 | webdav_save_config?: PartialWebDAVConfig; 43 | // onedrive_save_config?: PartialOneDriveConfig; 44 | save_to_local_per_update?: number; 45 | welcome_page_version?: string; 46 | } 47 | -------------------------------------------------------------------------------- /src/assets/icon.svg: -------------------------------------------------------------------------------- 1 | Wondicon - UI (Free) -------------------------------------------------------------------------------- /src/i18n/index.ts: -------------------------------------------------------------------------------- 1 | import { createI18n } from 'vue-i18n' 2 | import { resolveResource } from '@tauri-apps/api/path' 3 | import { invoke } from '@tauri-apps/api/core'; 4 | // 定义支持的语言类型 5 | export type Language = 'zh-Hans' | 'zh-Hant' | 'en' 6 | 7 | // 定义一个包含所有可用语言的数组,方便进行校验 8 | export const supportedLanguages: Language[] = ['zh-Hans', 'zh-Hant', 'en'] 9 | 10 | // 动态加载翻译文件的函数 11 | export const loadLocaleMessages = async (locale: Language) => { 12 | try { 13 | const resource_path = await resolveResource(`locales/${locale}.json`); 14 | console.log(resource_path) 15 | const content = await invoke('command_read_file', {path: resource_path}); 16 | return JSON.parse(content); 17 | } catch (error) { 18 | console.error(`Error loading locale ${locale}:`, error) 19 | // 返回空对象作为fallback 20 | return {} 21 | } 22 | } 23 | 24 | // 创建i18n实例,初始时使用空的messages 25 | const i18n = createI18n({ 26 | legacy: false, 27 | locale: 'zh-Hans', 28 | fallbackLocale: 'zh-Hans', 29 | messages: { 30 | 'zh-Hans': {}, 31 | 'zh-Hant': {}, 32 | 'en': {} 33 | }, 34 | globalInjection: true 35 | }) 36 | 37 | // 异步初始化默认语言 38 | const initializeDefaultLanguage = async () => { 39 | try { 40 | const defaultMessages = await loadLocaleMessages('zh-Hans') 41 | i18n.global.setLocaleMessage('zh-Hans', defaultMessages) 42 | } catch (error) { 43 | console.error('Failed to load default language:', error) 44 | } 45 | } 46 | 47 | // 立即初始化默认语言 48 | initializeDefaultLanguage() 49 | 50 | export default i18n 51 | 52 | // 初始化语言设置,从配置中读取 53 | export const initializeLanguage = async (language: string) => { 54 | try { 55 | if (language && (supportedLanguages as string[]).includes(language)) { 56 | const configLanguage = language as Language 57 | if (configLanguage !== i18n.global.locale.value) { 58 | // 动态加载新语言的翻译文件 59 | const messages = await loadLocaleMessages(configLanguage) 60 | i18n.global.setLocaleMessage(configLanguage, messages) 61 | i18n.global.locale.value = configLanguage as any 62 | console.log("成功设置语言: ", configLanguage); 63 | } 64 | } 65 | } catch (error) { 66 | console.warn('Failed to initialize language from config:', error) 67 | } 68 | } 69 | 70 | // 导出获取当前语言的函数 71 | export const getCurrentLanguage = (): Language => { 72 | // 函数的返回值类型也更精确了 73 | return i18n.global.locale.value 74 | } -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue"; 2 | import { createPinia } from "pinia"; 3 | import ElementPlus from 'element-plus'; 4 | import 'element-plus/dist/index.css'; 5 | import router from './router.ts'; 6 | import Router from "./views/Router.vue"; 7 | import i18n from './i18n'; 8 | 9 | const app = createApp(Router); 10 | const pinia = createPinia() 11 | app.use(pinia) 12 | app.use(ElementPlus); 13 | app.use(router); 14 | app.use(i18n); 15 | app.mount("#app"); 16 | -------------------------------------------------------------------------------- /src/router.ts: -------------------------------------------------------------------------------- 1 | // src/router.ts 2 | import { createRouter, createWebHistory } from 'vue-router'; 3 | import App from './views/App.vue'; 4 | import SettingWindow from './views/SettingWindow.vue'; 5 | import Welcome from './views/welcome.vue'; 6 | 7 | const routes = [ 8 | { path: '/', component: App }, 9 | { path: '/setting_window', component: SettingWindow }, 10 | { path: '/welcome', component: Welcome }, 11 | ]; 12 | 13 | const router = createRouter({ 14 | history: createWebHistory(), 15 | routes, 16 | }); 17 | 18 | export default router; -------------------------------------------------------------------------------- /src/stores/local_config.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { 3 | LocalConfig, 4 | LocalSaveConfig, 5 | WebDAVConfig, 6 | // OneDriveConfig, 7 | PartialLocalConfig, 8 | StorageDestination 9 | } from '../api/local_config_types' 10 | import { invoke } from '@tauri-apps/api/core' 11 | 12 | // 合并完整配置的辅助函数 13 | function mergeConfig(config: LocalConfig, partial: PartialLocalConfig): LocalConfig { 14 | return { 15 | // 保留原配置的所有属性 16 | ...config, 17 | // 覆盖顶层基本属性 18 | ...(partial.storage_destination !== undefined ? { storage_destination: partial.storage_destination } : {}), 19 | ...(partial.save_to_local_per_update !== undefined ? { save_to_local_per_update: partial.save_to_local_per_update } : {}), 20 | ...(partial.welcome_page_version !== undefined ? { welcome_page_version: partial.welcome_page_version } : {}), 21 | 22 | // 浅层合并嵌套对象,而不是完全替换 23 | local_save_config: partial.local_save_config !== undefined 24 | ? { ...config.local_save_config, ...partial.local_save_config } 25 | : config.local_save_config, 26 | 27 | webdav_save_config: partial.webdav_save_config !== undefined 28 | ? { ...config.webdav_save_config, ...partial.webdav_save_config } 29 | : config.webdav_save_config, 30 | 31 | // onedrive_save_config: partial.onedrive_save_config !== undefined 32 | // ? { ...config.onedrive_save_config, ...partial.onedrive_save_config } 33 | // : config.onedrive_save_config, 34 | }; 35 | } 36 | 37 | // 合并两个 partial 配置的辅助函数 38 | function mergePartialConfig( 39 | oldPartial: PartialLocalConfig, 40 | newPartial: PartialLocalConfig 41 | ): PartialLocalConfig { 42 | // 创建结果对象 43 | const result: PartialLocalConfig = { ...oldPartial }; 44 | 45 | // 合并顶层基本属性 46 | if (newPartial.storage_destination !== undefined) { 47 | result.storage_destination = newPartial.storage_destination; 48 | } 49 | 50 | if (newPartial.save_to_local_per_update !== undefined) { 51 | result.save_to_local_per_update = newPartial.save_to_local_per_update; 52 | } 53 | 54 | if (newPartial.welcome_page_version !== undefined) { 55 | result.welcome_page_version = newPartial.welcome_page_version; 56 | } 57 | 58 | // 浅层合并嵌套对象 59 | if (newPartial.local_save_config !== undefined) { 60 | result.local_save_config = { 61 | ...(oldPartial.local_save_config || {}), 62 | ...newPartial.local_save_config 63 | }; 64 | } 65 | 66 | if (newPartial.webdav_save_config !== undefined) { 67 | result.webdav_save_config = { 68 | ...(oldPartial.webdav_save_config || {}), 69 | ...newPartial.webdav_save_config 70 | }; 71 | } 72 | 73 | // if (newPartial.onedrive_save_config !== undefined) { 74 | // result.onedrive_save_config = { 75 | // ...(oldPartial.onedrive_save_config || {}), 76 | // ...newPartial.onedrive_save_config 77 | // }; 78 | // } 79 | 80 | return result; 81 | } 82 | 83 | export const useLocalConfigStore = defineStore('localConfig', { 84 | state: () => ({ 85 | config: { 86 | storage_destination: "Local" as StorageDestination, 87 | local_save_config: { 88 | destination_dir: "" 89 | } as LocalSaveConfig, 90 | webdav_save_config: { 91 | host_url: "", 92 | account: "", 93 | password: "", 94 | destination_dir: "" 95 | } as WebDAVConfig, 96 | // onedrive_save_config: { 97 | // refresh_token: "", 98 | // destination_dir: "" 99 | // } as OneDriveConfig, 100 | save_to_local_per_update: 0 101 | } as LocalConfig, 102 | dirtyConfig: {} as PartialLocalConfig 103 | }), 104 | 105 | actions: { 106 | // 从后端加载完整配置 107 | async loadConfig() { 108 | try { 109 | const config = await invoke('command_load_local_config') 110 | this.config = mergeConfig(this.config, config) 111 | } catch (error) { 112 | console.error("加载配置失败:", error) 113 | } 114 | }, 115 | 116 | // 更新配置 117 | updateConfig(partial: PartialLocalConfig) { 118 | console.log("收到消息:", partial) 119 | // 更新合并后的配置 120 | this.config = mergeConfig(this.config, partial) 121 | // 合并脏数据配置 122 | this.dirtyConfig = mergePartialConfig(this.dirtyConfig, partial) 123 | }, 124 | 125 | // 同步配置到后端 126 | async syncConfig() { 127 | if (Object.keys(this.dirtyConfig).length === 0) return 128 | 129 | try { 130 | await invoke("command_save_local_config", { partialConfig: this.dirtyConfig }) 131 | this.dirtyConfig = {} 132 | await this.loadConfig() 133 | } catch (error) { 134 | console.error("同步配置失败:", error) 135 | throw error // 抛出错误以便 UI 处理 136 | } 137 | } 138 | }, 139 | }) -------------------------------------------------------------------------------- /src/utils/ShortcutInput.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 129 | 130 | -------------------------------------------------------------------------------- /src/utils/color.ts: -------------------------------------------------------------------------------- 1 | function reduceOpacity(color: string, factor: number = 0.5): string { 2 | // 验证输入颜色格式 3 | console.log(color) 4 | if (!/^#[0-9A-Fa-f]{8}$/.test(color)) { 5 | throw new Error('颜色格式必须为 #RRGGBBAA'); 6 | } 7 | 8 | // 验证因子范围 9 | if (factor < 0 || factor > 1) { 10 | throw new Error('透明度因子必须在 0-1 范围内'); 11 | } 12 | 13 | // 提取颜色的 RGB 和 Alpha 部分 14 | const r = color.substring(1, 3); 15 | const g = color.substring(3, 5); 16 | const b = color.substring(5, 7); 17 | const a = color.substring(7, 9); 18 | 19 | // 将 Alpha 值从十六进制转换为十进制 20 | const alphaDecimal = parseInt(a, 16); 21 | 22 | // 计算新的 Alpha 值(降低透明度) 23 | const newAlphaDecimal = Math.max(0, Math.floor(alphaDecimal * factor)); 24 | 25 | // 将新的 Alpha 值转换回十六进制,并确保是两位数 26 | const newAlphaHex = newAlphaDecimal.toString(16).padStart(2, '0'); 27 | 28 | // 返回新的 RGBA 颜色 29 | return `#${r}${g}${b}${newAlphaHex}`; 30 | } 31 | 32 | function rgbaToHex(color: string): string { 33 | // 检查是否以 "rgba(" 开头并以 ")" 结尾 34 | if (!color.toLowerCase().startsWith('rgba(') || !color.endsWith(')')) { 35 | return color; 36 | } 37 | 38 | // 提取括号内的内容 39 | const content = color.substring(5, color.length - 1); 40 | 41 | // 分割 RGBA 值 42 | const parts = content.split(','); 43 | 44 | // 确保有 4 个部分 (r, g, b, a) 45 | if (parts.length !== 4) { 46 | return color; 47 | } 48 | 49 | // 解析 RGBA 值 50 | const r = parseInt(parts[0].trim(), 10); 51 | const g = parseInt(parts[1].trim(), 10); 52 | const b = parseInt(parts[2].trim(), 10); 53 | const a = parseFloat(parts[3].trim()); 54 | 55 | // 检查值是否有效 56 | if (isNaN(r) || isNaN(g) || isNaN(b) || isNaN(a) || 57 | r < 0 || r > 255 || g < 0 || g > 255 || b < 0 || b > 255 || a < 0 || a > 1) { 58 | return color; 59 | } 60 | 61 | // 将 RGB 转换为 HEX 62 | const toHex = (value: number): string => { 63 | const hex = value.toString(16); 64 | return hex.length === 1 ? '0' + hex : hex; 65 | }; 66 | 67 | // 将 Alpha 值(0-1)转换为 HEX(00-FF) 68 | const alphaHex = Math.round(a * 255).toString(16).padStart(2, '0'); 69 | 70 | // 返回 HEX 格式 (#RRGGBBAA) 71 | return `#${toHex(r)}${toHex(g)}${toHex(b)}${alphaHex}`; 72 | } 73 | 74 | export { reduceOpacity, rgbaToHex }; -------------------------------------------------------------------------------- /src/views/Router.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/views/UIConfigSetting.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 25 | 26 | -------------------------------------------------------------------------------- /src/views/components/AnimatedInput.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 90 | 91 | -------------------------------------------------------------------------------- /src/views/components/WindowSettings.vue: -------------------------------------------------------------------------------- 1 | 85 | 86 | 97 | 98 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module "*.vue" { 4 | import type { DefineComponent } from "vue"; 5 | const component: DefineComponent<{}, {}, any>; 6 | export default component; 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "preserve", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], 24 | "references": [{ "path": "./tsconfig.node.json" }] 25 | } 26 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true, 8 | "types": ["node"] 9 | }, 10 | "include": ["vite.config.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /utils/convert_svg_to_icon.py: -------------------------------------------------------------------------------- 1 | import os 2 | import cairosvg 3 | from PIL import Image 4 | import subprocess 5 | import platform 6 | import sys 7 | import re 8 | 9 | def ensure_directory(directory): 10 | """确保目录存在,如果不存在则创建""" 11 | if not os.path.exists(directory): 12 | os.makedirs(directory) 13 | 14 | def svg_to_png(svg_path, output_path, width, height): 15 | """将 SVG 转换为指定尺寸的 PNG""" 16 | cairosvg.svg2png(url=svg_path, write_to=output_path, output_width=width, output_height=height) 17 | 18 | # 使用 PIL 设置 DPI 为 72 19 | img = Image.open(output_path) 20 | img.save(output_path, dpi=(72, 72)) 21 | print(f"已创建: {output_path} (72 DPI)") 22 | 23 | def create_retina_image(png_path, output_path, scale=2): 24 | """创建 Retina (@2x) 版本的图像""" 25 | img = Image.open(png_path) 26 | width, height = img.size 27 | retina_img = img.resize((width * scale, height * scale), Image.LANCZOS) 28 | retina_img.save(output_path, dpi=(72, 72)) 29 | print(f"已创建: {output_path} (72 DPI)") 30 | 31 | def create_ico(png_path, output_path): 32 | """创建 .ico 文件""" 33 | # 为每个尺寸创建 72 DPI 的临时 PNG 文件 34 | temp_files = [] 35 | sizes = [ (128, 128)] 36 | 37 | img = Image.open(png_path) 38 | for size in sizes: 39 | temp_file = f"temp_{size[0]}x{size[1]}.png" 40 | temp_img = img.resize(size, Image.LANCZOS) 41 | temp_img.save(temp_file, dpi=(72, 72)) 42 | temp_files.append(temp_file) 43 | 44 | # 使用第一个临时文件作为基础,添加其他尺寸 45 | base_img = Image.open(temp_files[0]) 46 | other_imgs = [Image.open(f) for f in temp_files[1:]] 47 | 48 | # 保存为 ICO 49 | base_img.save(output_path, format='ICO', append_images=other_imgs, 50 | sizes=sizes) 51 | 52 | # 清理临时文件 53 | for file in temp_files: 54 | os.remove(file) 55 | 56 | print(f"已创建: {output_path}") 57 | 58 | def create_icns(svg_path, output_path): 59 | """创建 .icns 文件 (仅在 macOS 上使用 iconutil)""" 60 | # 创建临时目录 61 | temp_iconset = "icon.iconset" 62 | ensure_directory(temp_iconset) 63 | 64 | # 创建不同尺寸的图标 65 | sizes = [16, 32, 64, 128, 256, 512, 1024] 66 | for size in sizes: 67 | # 标准尺寸 68 | standard_output = os.path.join(temp_iconset, f"icon_{size}x{size}.png") 69 | svg_to_png(svg_path, standard_output, size, size) 70 | 71 | # Retina 尺寸 72 | if size < 512: # 1024 不需要 @2x 版本 73 | retina_output = os.path.join(temp_iconset, f"icon_{size}x{size}@2x.png") 74 | svg_to_png(svg_path, retina_output, size * 2, size * 2) 75 | 76 | # 使用 iconutil 创建 .icns 文件 (仅在 macOS 上) 77 | if platform.system() == 'Darwin': 78 | subprocess.run(['iconutil', '-c', 'icns', temp_iconset, '-o', output_path]) 79 | print(f"已创建: {output_path}") 80 | else: 81 | print("警告: 创建 .icns 文件需要 macOS 系统和 iconutil 工具") 82 | 83 | # 清理临时目录 84 | for file in os.listdir(temp_iconset): 85 | os.remove(os.path.join(temp_iconset, file)) 86 | os.rmdir(temp_iconset) 87 | 88 | def create_white_icon(svg_path, output_path, width, height, original_color, new_color="#ffffff"): 89 | """创建白色版本的图标""" 90 | # 读取SVG文件 91 | with open(svg_path, 'r') as file: 92 | svg_content = file.read() 93 | 94 | # 替换颜色 95 | white_svg_content = svg_content.replace(original_color, new_color) 96 | 97 | # 创建临时SVG文件 98 | temp_svg_path = "temp_white_icon.svg" 99 | with open(temp_svg_path, 'w') as file: 100 | file.write(white_svg_content) 101 | 102 | # 转换为PNG 103 | cairosvg.svg2png(url=temp_svg_path, write_to=output_path, output_width=width, output_height=height) 104 | 105 | # 使用PIL设置DPI为72 106 | img = Image.open(output_path) 107 | img.save(output_path, dpi=(72, 72)) 108 | 109 | # 删除临时SVG文件 110 | os.remove(temp_svg_path) 111 | 112 | print(f"已创建白色图标: {output_path} (72 DPI)") 113 | 114 | def main(): 115 | # 输入 SVG 文件 116 | svg_file = "icon.svg" 117 | 118 | # 检查文件是否存在 119 | if not os.path.exists(svg_file): 120 | print(f"错误: 找不到文件 {svg_file}") 121 | sys.exit(1) 122 | 123 | # 创建输出目录 124 | output_dir = "output" 125 | ensure_directory(output_dir) 126 | 127 | # 创建标准 PNG 图标 128 | standard_png = os.path.join(output_dir, "icon.png") 129 | svg_to_png(svg_file, standard_png, 1024, 1024) # 创建一个高分辨率的基础 PNG 130 | 131 | # 创建指定尺寸的 PNG 图标 132 | sizes = { 133 | "32x32.png": (32, 32), 134 | "128x128.png": (128, 128), 135 | "Square30x30Logo.png": (30, 30), 136 | "Square44x44Logo.png": (44, 44), 137 | "Square71x71Logo.png": (71, 71), 138 | "Square89x89Logo.png": (89, 89), 139 | "Square107x107Logo.png": (107, 107), 140 | "Square142x142Logo.png": (142, 142), 141 | "Square150x150Logo.png": (150, 150), 142 | "Square284x284Logo.png": (284, 284), 143 | "Square310x310Logo.png": (310, 310), 144 | "StoreLogo.png": (50, 50) # 假设 StoreLogo 为 50x50 145 | } 146 | 147 | for filename, (width, height) in sizes.items(): 148 | output_path = os.path.join(output_dir, filename) 149 | svg_to_png(svg_file, output_path, width, height) 150 | 151 | # 创建 Retina (@2x) 版本 152 | retina_path = os.path.join(output_dir, "128x128@2x.png") 153 | svg_to_png(svg_file, retina_path, 256, 256) # 直接从 SVG 创建 2x 尺寸 154 | 155 | # 创建 .ico 文件 156 | ico_path = os.path.join(output_dir, "icon.ico") 157 | create_ico(standard_png, ico_path) 158 | 159 | # 创建 .icns 文件 (仅在 macOS 上) 160 | icns_path = os.path.join(output_dir, "icon.icns") 161 | create_icns(svg_file, icns_path) 162 | 163 | white_icon_path = os.path.join(output_dir, "32x32-white.png") 164 | create_white_icon(svg_file, white_icon_path, 32, 32, original_color="#231f20") 165 | 166 | print("所有图标已成功创建,分辨率均为 72 DPI!") 167 | 168 | 169 | 170 | if __name__ == "__main__": 171 | main() -------------------------------------------------------------------------------- /utils/icon.svg: -------------------------------------------------------------------------------- 1 | Wondicon - UI (Free) -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import vue from "@vitejs/plugin-vue"; 3 | import { copyFileSync, mkdirSync, existsSync, readdirSync } from "fs"; 4 | import { join } from "path"; 5 | 6 | const host = process.env.TAURI_DEV_HOST; 7 | 8 | // https://vitejs.dev/config/ 9 | // 自定义插件:复制i18n locales到src-tauri 10 | const copyI18nPlugin = () => { 11 | return { 12 | name: 'copy-i18n-locales', 13 | buildStart() { 14 | const srcLocalesDir = join(process.cwd(), 'src', 'i18n', 'locales'); 15 | const destDir = join(process.cwd(), 'src-tauri', 'locales'); 16 | 17 | // 创建目标目录 18 | if (!existsSync(destDir)) { 19 | mkdirSync(destDir, { recursive: true }); 20 | } 21 | if (!existsSync(destDir)) { 22 | mkdirSync(destDir, { recursive: true }); 23 | } 24 | // 复制locales文件夹中的所有文件 25 | try { 26 | const files = readdirSync(srcLocalesDir); 27 | files.forEach(file => { 28 | const srcFile = join(srcLocalesDir, file); 29 | const destFile = join(destDir, file); 30 | copyFileSync(srcFile, destFile); 31 | }); 32 | console.log(`✓ ${files.length} i18n locales files copied to src-tauri/locales/`); 33 | } catch (error) { 34 | console.error('Failed to copy i18n locales:', error); 35 | } 36 | } 37 | }; 38 | }; 39 | 40 | export default defineConfig(async () => ({ 41 | plugins: [vue(), copyI18nPlugin()], 42 | 43 | // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` 44 | // 45 | // 1. prevent vite from obscuring rust errors 46 | clearScreen: false, 47 | // 2. tauri expects a fixed port, fail if that port is not available 48 | server: { 49 | port: 12345, 50 | strictPort: true, 51 | host: host || false, 52 | hmr: host 53 | ? { 54 | protocol: "ws", 55 | host, 56 | port: 1421, 57 | } 58 | : undefined, 59 | watch: { 60 | // 3. tell vite to ignore watching `src-tauri` 61 | ignored: ["**/src-tauri/**"], 62 | }, 63 | }, 64 | })); 65 | -------------------------------------------------------------------------------- /xtask/.gitignore: -------------------------------------------------------------------------------- 1 | target -------------------------------------------------------------------------------- /xtask/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "xtask" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | clap = { version = "4.5", features = ["derive"] } 8 | tokio = { version = "1.47", features = ["full"] } 9 | anyhow = "1.0" 10 | zip = "5.1" 11 | serde = { version = "1.0", features = ["derive"] } 12 | serde_json = "1.0" 13 | -------------------------------------------------------------------------------- /xtask/README.md: -------------------------------------------------------------------------------- 1 | # ZeroLaunch-rs 自动化构建工具 2 | 3 | 这是一个用于自动化构建 ZeroLaunch-rs 项目的强大工具,支持构建安装包版本和便携版本,包括 x64 和 ARM64 架构,并提供灵活的架构选择功能。 4 | 5 | ## 🚀 功能特性 6 | 7 | - ✨ **架构选择**:支持选择性构建特定架构(x64、ARM64 或全部) 8 | - 🧠 **AI 特性开关**:支持构建完全体(启用 AI)或精简版(关闭 AI) 9 | - 📦 **多版本支持**:安装包版本和便携版本 10 | - 🏗️ **多架构支持**:x64 和 ARM64 架构 11 | - 📁 **自动打包**:便携版本自动打包为 ZIP 文件 12 | - 🧹 **智能清理**:清理构建产物和临时文件 13 | - ⚡ **高效构建**:可选择性构建,节省时间 14 | 15 | ## 📋 使用方法 16 | 17 | ### 基本命令 18 | 19 | #### 构建所有版本(默认:所有架构 & 同时构建 启用/关闭 AI 两种模式) 20 | ```bash 21 | cargo run --bin xtask build-all 22 | ``` 23 | 24 | #### 仅构建启用 AI 的完全体(只生成含 AI 版本) 25 | ```bash 26 | cargo run --bin xtask build-all --ai enabled 27 | ``` 28 | 29 | #### 仅构建关闭 AI 的精简版 30 | ```bash 31 | cargo run --bin xtask build-all --ai disabled 32 | ``` 33 | 34 | #### 构建安装包(默认:启用 AI) 35 | ```bash 36 | cargo run --bin xtask build-installer 37 | ``` 38 | 39 | #### 构建安装包版本(显式关闭 AI) 40 | ```bash 41 | cargo run --bin xtask build-installer --ai disabled 42 | ``` 43 | 44 | #### 构建安装包版本(默认:所有架构,默认启用 AI) 45 | ```bash 46 | cargo run --bin xtask build-installer 47 | ``` 48 | 49 | #### 构建便携版本(默认:所有架构,默认启用 AI) 50 | ```bash 51 | cargo run --bin xtask build-portable 52 | ``` 53 | 54 | #### 构建便携版本(关闭 AI) 55 | ```bash 56 | cargo run --bin xtask build-portable --ai disabled 57 | ``` 58 | 59 | #### 清理构建产物 60 | ```bash 61 | cargo run --bin xtask clean 62 | ``` 63 | 64 | ### 🎯 架构选择功能 65 | 66 | #### 仅构建 x64 架构 67 | ```bash 68 | # 构建所有版本的 x64 架构 69 | cargo run --bin xtask build-all --arch x64 70 | 71 | # 仅构建 x64 安装包 72 | cargo run --bin xtask build-installer --arch x64 73 | 74 | # 仅构建 x64 便携版 75 | cargo run --bin xtask build-portable --arch x64 76 | ``` 77 | 78 | #### 仅构建 ARM64 架构 79 | ```bash 80 | # 构建所有版本的 ARM64 架构 81 | cargo run --bin xtask build-all --arch arm64 82 | 83 | # 仅构建 ARM64 安装包 84 | cargo run --bin xtask build-installer --arch arm64 85 | 86 | # 仅构建 ARM64 便携版 87 | cargo run --bin xtask build-portable --arch arm64 88 | ``` 89 | 90 | #### 构建所有架构(显式指定) 91 | ```bash 92 | # 明确指定构建所有架构 93 | cargo run --bin xtask build-all --arch all 94 | ``` 95 | 96 | ### 🤖 AI 特性开关 97 | 98 | 当前默认策略: 99 | 100 | * build-installer / build-portable 不加 `--ai` 参数时 = 启用 AI(完全体) 101 | * 如需精简 Lite 版,加 `--ai disabled` 102 | * build-all 默认同时构建 enabled + disabled 两种产物,可改为单一模式 103 | 104 | ```bash 105 | # 构建默认完全体(启用 AI) 106 | cargo run --bin xtask build-installer 107 | 108 | # 构建精简版(关闭 AI) 109 | cargo run --bin xtask build-installer --ai disabled 110 | 111 | # 便携版默认同样启用 AI 112 | cargo run --bin xtask build-portable 113 | 114 | # 构建便携版精简版 115 | cargo run --bin xtask build-portable --ai disabled 116 | 117 | # build-all 默认同时构建启用与关闭 AI 的两个版本,可显式限制 118 | cargo run --bin xtask build-all --ai both 119 | ``` 120 | 121 | ### 📖 参数说明 122 | 123 | | 参数 | 简写 | 可选值 | 默认值 | 说明 | 124 | |------|------|--------|--------|---------| 125 | | `--arch` | `-a` | `x64`, `arm64`, `all` | `all` | 指定构建的目标架构 | 126 | | `--ai` | - | `enabled`, `disabled`, `both`(仅 `build-all`) | `enabled`(`build-all` 默认 `both`) | 是否启用 AI 特性:安装包/便携版默认构建启用 AI;Lite 需显式 `--ai disabled` | 127 | 128 | ### 💡 使用场景示例 129 | 130 | ```bash 131 | # 快速构建:仅构建当前平台的 x64 便携版 132 | cargo run --bin xtask build-portable -a x64 133 | 134 | # 发布准备:构建所有版本的所有架构(包含启用/关闭 AI) 135 | cargo run --bin xtask build-all 136 | 137 | # 测试构建:仅构建 ARM64 安装包 138 | cargo run --bin xtask build-installer --arch arm64 139 | 140 | # 清理后重新构建 x64 版本 141 | cargo run --bin xtask clean 142 | cargo run --bin xtask build-all -a x64 143 | ``` 144 | 145 | ## 📦 构建产物 146 | 147 | ### 安装包版本 148 | 构建完成后,安装包会自动移动到项目根目录: 149 | - `zerolaunch-rs_0.5.1_x64-setup.exe` / `ZeroLaunch_0.5.1_x64_en-US.msi` 等 —— 启用 AI 的完全体命名(示例) 150 | - `zerolaunch-rs_lite_0.5.1_x64-setup.exe` / `ZeroLaunch_lite_0.5.1_x64_en-US.msi` 等 —— 关闭 AI 的精简版命名(示例) 151 | - ARM64 架构的安装包命名同理,会在架构字段前插入 `_lite` 152 | 153 | ### 便携版本 154 | 便携版会打包成 ZIP 文件并放置在项目根目录: 155 | - `ZeroLaunch-portable-0.5.1-x64.zip` - 启用 AI 的 x64 便携版 ZIP 包 156 | - `ZeroLaunch-portable-lite-0.5.1-x64.zip` - 关闭 AI 的 x64 便携版 ZIP 包 157 | - `ZeroLaunch-portable-0.5.1-arm64.zip` / `ZeroLaunch-portable-lite-0.5.1-arm64.zip` - ARM64 架构同理 158 | 159 | 便携版 ZIP 包包含: 160 | - 主程序可执行文件 161 | - `icons/` 文件夹(图标资源) 162 | - `locales/` 文件夹(国际化文件,如果存在) 163 | 164 | ## 🔧 故障排除 165 | 166 | ### 常见问题 167 | 168 | 1. **ARM64 构建失败** 169 | ```bash 170 | # 安装 ARM64 目标平台 171 | rustup target add aarch64-pc-windows-msvc 172 | ``` 173 | 174 | 2. **构建时间过长** 175 | - 使用架构选择功能仅构建需要的架构 176 | - 确保系统有足够的内存和存储空间 177 | 178 | 3. **权限问题** 179 | - 确保对项目目录有写权限 180 | - 在 Windows 上可能需要管理员权限 181 | 182 | ## 📄 许可证 183 | 184 | 本项目遵循与 ZeroLaunch-rs 主项目相同的许可证。 185 | 186 | --- 187 | 188 | **提示**:首次构建可能需要较长时间,因为需要下载依赖和编译。建议使用架构选择功能来加速开发过程中的构建。 --------------------------------------------------------------------------------