├── src-tauri ├── src │ ├── utils │ │ ├── mod.rs │ │ └── protobuf.rs │ ├── main.rs │ ├── models │ │ ├── mod.rs │ │ ├── config.rs │ │ ├── token.rs │ │ ├── quota.rs │ │ └── account.rs │ ├── modules │ │ ├── mod.rs │ │ ├── config.rs │ │ ├── i18n.rs │ │ ├── db.rs │ │ ├── logger.rs │ │ ├── oauth_server.rs │ │ ├── quota.rs │ │ └── oauth.rs │ ├── error.rs │ └── lib.rs ├── build.rs ├── icons │ ├── 32x32.png │ ├── 64x64.png │ ├── icon.icns │ ├── icon.ico │ ├── icon.png │ ├── 128x128.png │ ├── 128x128@2x.png │ ├── StoreLogo.png │ ├── tray-icon.png │ ├── Square30x30Logo.png │ ├── Square44x44Logo.png │ ├── Square71x71Logo.png │ ├── Square89x89Logo.png │ ├── Square107x107Logo.png │ ├── Square142x142Logo.png │ ├── Square150x150Logo.png │ ├── Square284x284Logo.png │ ├── Square310x310Logo.png │ ├── ios │ │ ├── AppIcon-512@2x.png │ │ ├── AppIcon-20x20@1x.png │ │ ├── AppIcon-20x20@2x.png │ │ ├── AppIcon-20x20@3x.png │ │ ├── AppIcon-29x29@1x.png │ │ ├── AppIcon-29x29@2x.png │ │ ├── AppIcon-29x29@3x.png │ │ ├── AppIcon-40x40@1x.png │ │ ├── AppIcon-40x40@2x.png │ │ ├── AppIcon-40x40@3x.png │ │ ├── AppIcon-60x60@2x.png │ │ ├── AppIcon-60x60@3x.png │ │ ├── AppIcon-76x76@1x.png │ │ ├── AppIcon-76x76@2x.png │ │ ├── AppIcon-20x20@2x-1.png │ │ ├── AppIcon-29x29@2x-1.png │ │ ├── AppIcon-40x40@2x-1.png │ │ └── AppIcon-83.5x83.5@2x.png │ ├── icon_master_squircle.png │ ├── icon.iconset │ │ ├── icon_16x16.png │ │ ├── icon_32x32.png │ │ ├── icon_128x128.png │ │ ├── icon_256x256.png │ │ ├── icon_512x512.png │ │ ├── icon_128x128@2x.png │ │ ├── icon_16x16@2x.png │ │ ├── icon_256x256@2x.png │ │ ├── icon_32x32@2x.png │ │ └── icon_512x512@2x.png │ └── android │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_round.png │ │ └── ic_launcher_foreground.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_round.png │ │ └── ic_launcher_foreground.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_round.png │ │ └── ic_launcher_foreground.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_round.png │ │ └── ic_launcher_foreground.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_round.png │ │ └── ic_launcher_foreground.png │ │ ├── values │ │ └── ic_launcher_background.xml │ │ └── mipmap-anydpi-v26 │ │ └── ic_launcher.xml ├── .gitignore ├── capabilities │ └── default.json ├── tauri.conf.json ├── Cargo.toml ├── output_check_final.txt ├── output_check_owned.txt ├── output_check.txt ├── output_check_final_4.txt ├── output_check_final_3.txt ├── output_check_tray.txt └── output_check_final_2.txt ├── src ├── vite-env.d.ts ├── types │ ├── config.ts │ └── account.ts ├── services │ ├── configService.ts │ └── accountService.ts ├── main.tsx ├── components │ ├── dashboard │ │ ├── StatsCard.tsx │ │ ├── BestAccounts.tsx │ │ └── CurrentAccount.tsx │ ├── layout │ │ ├── Layout.tsx │ │ └── Navbar.tsx │ ├── common │ │ ├── ToastContainer.tsx │ │ ├── ThemeManager.tsx │ │ ├── Toast.tsx │ │ ├── BackgroundTaskRunner.tsx │ │ ├── ModalDialog.tsx │ │ └── Pagination.tsx │ └── accounts │ │ ├── AccountGrid.tsx │ │ ├── AccountTable.tsx │ │ └── AccountDetailsDialog.tsx ├── i18n.ts ├── stores │ ├── useConfigStore.ts │ └── useAccountStore.ts ├── utils │ └── format.ts ├── App.css ├── App.tsx ├── assets │ └── react.svg └── locales │ ├── zh.json │ └── en.json ├── public ├── icon.png ├── vite.svg └── tauri.svg ├── .vscode └── extensions.json ├── docs └── images │ ├── about-dark.png │ ├── accounts-dark.png │ ├── accounts-light.png │ ├── settings-dark.png │ ├── dashboard-light.png │ └── dashboard-preview.png ├── postcss.config.cjs ├── tsconfig.node.json ├── .gitignore ├── tsconfig.json ├── vite.config.ts ├── package.json ├── tailwind.config.js ├── .github └── workflows │ └── release.yml ├── index.html ├── scripts └── standardize_icon.py ├── test_tauri_commands.js ├── README.md └── README_EN.md /src-tauri/src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod protobuf; 2 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src-tauri/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | tauri_build::build() 3 | } 4 | -------------------------------------------------------------------------------- /public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbjlaq/Antigravity-Manager/HEAD/public/icon.png -------------------------------------------------------------------------------- /src-tauri/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbjlaq/Antigravity-Manager/HEAD/src-tauri/icons/32x32.png -------------------------------------------------------------------------------- /src-tauri/icons/64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbjlaq/Antigravity-Manager/HEAD/src-tauri/icons/64x64.png -------------------------------------------------------------------------------- /src-tauri/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbjlaq/Antigravity-Manager/HEAD/src-tauri/icons/icon.icns -------------------------------------------------------------------------------- /src-tauri/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbjlaq/Antigravity-Manager/HEAD/src-tauri/icons/icon.ico -------------------------------------------------------------------------------- /src-tauri/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbjlaq/Antigravity-Manager/HEAD/src-tauri/icons/icon.png -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"] 3 | } 4 | -------------------------------------------------------------------------------- /docs/images/about-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbjlaq/Antigravity-Manager/HEAD/docs/images/about-dark.png -------------------------------------------------------------------------------- /src-tauri/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbjlaq/Antigravity-Manager/HEAD/src-tauri/icons/128x128.png -------------------------------------------------------------------------------- /docs/images/accounts-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbjlaq/Antigravity-Manager/HEAD/docs/images/accounts-dark.png -------------------------------------------------------------------------------- /docs/images/accounts-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbjlaq/Antigravity-Manager/HEAD/docs/images/accounts-light.png -------------------------------------------------------------------------------- /docs/images/settings-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbjlaq/Antigravity-Manager/HEAD/docs/images/settings-dark.png -------------------------------------------------------------------------------- /src-tauri/icons/128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbjlaq/Antigravity-Manager/HEAD/src-tauri/icons/128x128@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/StoreLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbjlaq/Antigravity-Manager/HEAD/src-tauri/icons/StoreLogo.png -------------------------------------------------------------------------------- /src-tauri/icons/tray-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbjlaq/Antigravity-Manager/HEAD/src-tauri/icons/tray-icon.png -------------------------------------------------------------------------------- /docs/images/dashboard-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbjlaq/Antigravity-Manager/HEAD/docs/images/dashboard-light.png -------------------------------------------------------------------------------- /docs/images/dashboard-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbjlaq/Antigravity-Manager/HEAD/docs/images/dashboard-preview.png -------------------------------------------------------------------------------- /src-tauri/icons/Square30x30Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbjlaq/Antigravity-Manager/HEAD/src-tauri/icons/Square30x30Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square44x44Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbjlaq/Antigravity-Manager/HEAD/src-tauri/icons/Square44x44Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square71x71Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbjlaq/Antigravity-Manager/HEAD/src-tauri/icons/Square71x71Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square89x89Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbjlaq/Antigravity-Manager/HEAD/src-tauri/icons/Square89x89Logo.png -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /src-tauri/icons/Square107x107Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbjlaq/Antigravity-Manager/HEAD/src-tauri/icons/Square107x107Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square142x142Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbjlaq/Antigravity-Manager/HEAD/src-tauri/icons/Square142x142Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square150x150Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbjlaq/Antigravity-Manager/HEAD/src-tauri/icons/Square150x150Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square284x284Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbjlaq/Antigravity-Manager/HEAD/src-tauri/icons/Square284x284Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square310x310Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbjlaq/Antigravity-Manager/HEAD/src-tauri/icons/Square310x310Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbjlaq/Antigravity-Manager/HEAD/src-tauri/icons/ios/AppIcon-512@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/icon_master_squircle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbjlaq/Antigravity-Manager/HEAD/src-tauri/icons/icon_master_squircle.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbjlaq/Antigravity-Manager/HEAD/src-tauri/icons/ios/AppIcon-20x20@1x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbjlaq/Antigravity-Manager/HEAD/src-tauri/icons/ios/AppIcon-20x20@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbjlaq/Antigravity-Manager/HEAD/src-tauri/icons/ios/AppIcon-20x20@3x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbjlaq/Antigravity-Manager/HEAD/src-tauri/icons/ios/AppIcon-29x29@1x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbjlaq/Antigravity-Manager/HEAD/src-tauri/icons/ios/AppIcon-29x29@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbjlaq/Antigravity-Manager/HEAD/src-tauri/icons/ios/AppIcon-29x29@3x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbjlaq/Antigravity-Manager/HEAD/src-tauri/icons/ios/AppIcon-40x40@1x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbjlaq/Antigravity-Manager/HEAD/src-tauri/icons/ios/AppIcon-40x40@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbjlaq/Antigravity-Manager/HEAD/src-tauri/icons/ios/AppIcon-40x40@3x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbjlaq/Antigravity-Manager/HEAD/src-tauri/icons/ios/AppIcon-60x60@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbjlaq/Antigravity-Manager/HEAD/src-tauri/icons/ios/AppIcon-60x60@3x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbjlaq/Antigravity-Manager/HEAD/src-tauri/icons/ios/AppIcon-76x76@1x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbjlaq/Antigravity-Manager/HEAD/src-tauri/icons/ios/AppIcon-76x76@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/icon.iconset/icon_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbjlaq/Antigravity-Manager/HEAD/src-tauri/icons/icon.iconset/icon_16x16.png -------------------------------------------------------------------------------- /src-tauri/icons/icon.iconset/icon_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbjlaq/Antigravity-Manager/HEAD/src-tauri/icons/icon.iconset/icon_32x32.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-20x20@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbjlaq/Antigravity-Manager/HEAD/src-tauri/icons/ios/AppIcon-20x20@2x-1.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-29x29@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbjlaq/Antigravity-Manager/HEAD/src-tauri/icons/ios/AppIcon-29x29@2x-1.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-40x40@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbjlaq/Antigravity-Manager/HEAD/src-tauri/icons/ios/AppIcon-40x40@2x-1.png -------------------------------------------------------------------------------- /src-tauri/icons/icon.iconset/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbjlaq/Antigravity-Manager/HEAD/src-tauri/icons/icon.iconset/icon_128x128.png -------------------------------------------------------------------------------- /src-tauri/icons/icon.iconset/icon_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbjlaq/Antigravity-Manager/HEAD/src-tauri/icons/icon.iconset/icon_256x256.png -------------------------------------------------------------------------------- /src-tauri/icons/icon.iconset/icon_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbjlaq/Antigravity-Manager/HEAD/src-tauri/icons/icon.iconset/icon_512x512.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbjlaq/Antigravity-Manager/HEAD/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/icon.iconset/icon_128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbjlaq/Antigravity-Manager/HEAD/src-tauri/icons/icon.iconset/icon_128x128@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/icon.iconset/icon_16x16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbjlaq/Antigravity-Manager/HEAD/src-tauri/icons/icon.iconset/icon_16x16@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/icon.iconset/icon_256x256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbjlaq/Antigravity-Manager/HEAD/src-tauri/icons/icon.iconset/icon_256x256@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/icon.iconset/icon_32x32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbjlaq/Antigravity-Manager/HEAD/src-tauri/icons/icon.iconset/icon_32x32@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/icon.iconset/icon_512x512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbjlaq/Antigravity-Manager/HEAD/src-tauri/icons/icon.iconset/icon_512x512@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbjlaq/Antigravity-Manager/HEAD/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbjlaq/Antigravity-Manager/HEAD/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbjlaq/Antigravity-Manager/HEAD/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbjlaq/Antigravity-Manager/HEAD/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbjlaq/Antigravity-Manager/HEAD/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbjlaq/Antigravity-Manager/HEAD/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbjlaq/Antigravity-Manager/HEAD/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbjlaq/Antigravity-Manager/HEAD/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbjlaq/Antigravity-Manager/HEAD/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbjlaq/Antigravity-Manager/HEAD/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbjlaq/Antigravity-Manager/HEAD/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbjlaq/Antigravity-Manager/HEAD/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbjlaq/Antigravity-Manager/HEAD/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /src-tauri/icons/android/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #fff 4 | -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbjlaq/Antigravity-Manager/HEAD/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbjlaq/Antigravity-Manager/HEAD/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | antigravity_tools_lib::run() 6 | } 7 | -------------------------------------------------------------------------------- /src-tauri/src/models/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod account; 2 | pub mod token; 3 | pub mod quota; 4 | pub mod config; 5 | 6 | pub use account::{Account, AccountIndex, AccountSummary}; 7 | pub use token::TokenData; 8 | pub use quota::QuotaData; 9 | pub use config::AppConfig; 10 | -------------------------------------------------------------------------------- /src/types/config.ts: -------------------------------------------------------------------------------- 1 | export interface AppConfig { 2 | language: string; 3 | theme: string; 4 | auto_refresh: boolean; 5 | refresh_interval: number; 6 | auto_sync: boolean; 7 | sync_interval: number; 8 | default_export_path?: string; 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src-tauri/src/modules/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod account; 2 | pub mod quota; 3 | pub mod config; 4 | pub mod logger; 5 | pub mod db; 6 | pub mod process; 7 | pub mod oauth; 8 | pub mod oauth_server; 9 | pub mod migration; 10 | pub mod tray; 11 | pub mod i18n; 12 | 13 | pub use account::*; 14 | pub use quota::*; 15 | pub use config::*; 16 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /src/services/configService.ts: -------------------------------------------------------------------------------- 1 | import { invoke } from '@tauri-apps/api/core'; 2 | import { AppConfig } from '../types/config'; 3 | 4 | export async function loadConfig(): Promise { 5 | return await invoke('load_config'); 6 | } 7 | 8 | export async function saveConfig(config: AppConfig): Promise { 9 | return await invoke('save_config', { config }); 10 | } 11 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import { invoke } from "@tauri-apps/api/core"; 4 | 5 | import App from './App'; 6 | import './i18n'; // Import i18n config 7 | import "./App.css"; 8 | 9 | // 启动时显式调用 Rust 命令显示窗口 10 | // 配合 visible:false 使用,解决启动黑屏问题 11 | invoke("show_main_window").catch(console.error); 12 | 13 | ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( 14 | 15 | 16 | 17 | , 18 | ); 19 | -------------------------------------------------------------------------------- /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 | ], 8 | "permissions": [ 9 | "core:default", 10 | "core:window:default", 11 | "core:window:allow-start-dragging", 12 | "core:window:allow-set-background-color", 13 | "core:window:allow-show", 14 | "opener:default", 15 | "dialog:default", 16 | "fs:default", 17 | "core:tray:default" 18 | ] 19 | } -------------------------------------------------------------------------------- /src/types/account.ts: -------------------------------------------------------------------------------- 1 | export interface Account { 2 | id: string; 3 | email: string; 4 | name?: string; 5 | token: TokenData; 6 | quota?: QuotaData; 7 | created_at: number; 8 | last_used: number; 9 | } 10 | 11 | export interface TokenData { 12 | access_token: string; 13 | refresh_token: string; 14 | expires_in: number; 15 | expiry_timestamp: number; 16 | token_type: string; 17 | email?: string; 18 | } 19 | 20 | export interface QuotaData { 21 | models: ModelQuota[]; 22 | last_updated: number; 23 | is_forbidden?: boolean; 24 | } 25 | 26 | export interface ModelQuota { 27 | name: string; 28 | percentage: number; 29 | reset_time: string; 30 | } 31 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src"], 24 | "references": [{ "path": "./tsconfig.node.json" }] 25 | } 26 | -------------------------------------------------------------------------------- /src/components/dashboard/StatsCard.tsx: -------------------------------------------------------------------------------- 1 | import { LucideIcon } from 'lucide-react'; 2 | 3 | interface StatsCardProps { 4 | icon: LucideIcon; 5 | title: string; 6 | value: string | number; 7 | description?: string; 8 | colorClass?: string; 9 | } 10 | 11 | function StatsCard({ icon: Icon, title, value, description, colorClass = 'primary' }: StatsCardProps) { 12 | return ( 13 |
14 |
15 | 16 |
17 |
{title}
18 |
{value}
19 | {description &&
{description}
} 20 |
21 | ); 22 | } 23 | 24 | export default StatsCard; 25 | -------------------------------------------------------------------------------- /src-tauri/src/models/config.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | /// 应用配置 4 | #[derive(Debug, Clone, Serialize, Deserialize)] 5 | pub struct AppConfig { 6 | pub language: String, 7 | pub theme: String, 8 | pub auto_refresh: bool, 9 | pub refresh_interval: i32, // 分钟 10 | pub auto_sync: bool, 11 | pub sync_interval: i32, // 分钟 12 | pub default_export_path: Option, 13 | } 14 | 15 | impl AppConfig { 16 | pub fn new() -> Self { 17 | Self { 18 | language: "zh-CN".to_string(), 19 | theme: "system".to_string(), 20 | auto_refresh: false, 21 | refresh_interval: 15, 22 | auto_sync: false, 23 | sync_interval: 5, 24 | default_export_path: None, 25 | } 26 | } 27 | } 28 | 29 | impl Default for AppConfig { 30 | fn default() -> Self { 31 | Self::new() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | 4 | // @ts-expect-error process is a nodejs global 5 | const host = process.env.TAURI_DEV_HOST; 6 | 7 | // https://vite.dev/config/ 8 | export default defineConfig(async () => ({ 9 | plugins: [react()], 10 | 11 | // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` 12 | // 13 | // 1. prevent Vite from obscuring rust errors 14 | clearScreen: false, 15 | // 2. tauri expects a fixed port, fail if that port is not available 16 | server: { 17 | port: 1420, 18 | strictPort: true, 19 | host: host || false, 20 | hmr: host 21 | ? { 22 | protocol: "ws", 23 | host, 24 | port: 1421, 25 | } 26 | : undefined, 27 | watch: { 28 | // 3. tell Vite to ignore watching `src-tauri` 29 | ignored: ["**/src-tauri/**"], 30 | }, 31 | }, 32 | })); 33 | -------------------------------------------------------------------------------- /src-tauri/src/models/token.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | /// Token 数据结构 4 | #[derive(Debug, Clone, Serialize, Deserialize)] 5 | pub struct TokenData { 6 | pub access_token: String, 7 | pub refresh_token: String, 8 | pub expires_in: i64, 9 | pub expiry_timestamp: i64, 10 | pub token_type: String, 11 | pub email: Option, 12 | } 13 | 14 | impl TokenData { 15 | pub fn new( 16 | access_token: String, 17 | refresh_token: String, 18 | expires_in: i64, 19 | email: Option, 20 | ) -> Self { 21 | let expiry_timestamp = chrono::Utc::now().timestamp() + expires_in; 22 | Self { 23 | access_token, 24 | refresh_token, 25 | expires_in, 26 | expiry_timestamp, 27 | token_type: "Bearer".to_string(), 28 | email, 29 | } 30 | } 31 | 32 | #[allow(dead_code)] 33 | pub fn is_expired(&self) -> bool { 34 | chrono::Utc::now().timestamp() >= self.expiry_timestamp 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src-tauri/src/models/quota.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | /// 模型配额信息 4 | #[derive(Debug, Clone, Serialize, Deserialize)] 5 | pub struct ModelQuota { 6 | pub name: String, 7 | pub percentage: i32, // 剩余百分比 0-100 8 | pub reset_time: String, 9 | } 10 | 11 | /// 配额数据结构 12 | #[derive(Debug, Clone, Serialize, Deserialize)] 13 | pub struct QuotaData { 14 | pub models: Vec, 15 | pub last_updated: i64, 16 | #[serde(default)] 17 | pub is_forbidden: bool, 18 | } 19 | 20 | impl QuotaData { 21 | pub fn new() -> Self { 22 | Self { 23 | models: Vec::new(), 24 | last_updated: chrono::Utc::now().timestamp(), 25 | is_forbidden: false, 26 | } 27 | } 28 | 29 | pub fn add_model(&mut self, name: String, percentage: i32, reset_time: String) { 30 | self.models.push(ModelQuota { 31 | name, 32 | percentage, 33 | reset_time, 34 | }); 35 | } 36 | } 37 | 38 | impl Default for QuotaData { 39 | fn default() -> Self { 40 | Self::new() 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src-tauri/src/modules/config.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use serde_json; 3 | 4 | use crate::models::AppConfig; 5 | use super::account::get_data_dir; 6 | 7 | const CONFIG_FILE: &str = "gui_config.json"; 8 | 9 | /// 加载应用配置 10 | pub fn load_app_config() -> Result { 11 | let data_dir = get_data_dir()?; 12 | let config_path = data_dir.join(CONFIG_FILE); 13 | 14 | if !config_path.exists() { 15 | return Ok(AppConfig::new()); 16 | } 17 | 18 | let content = fs::read_to_string(&config_path) 19 | .map_err(|e| format!("读取配置文件失败: {}", e))?; 20 | 21 | serde_json::from_str(&content) 22 | .map_err(|e| format!("解析配置文件失败: {}", e)) 23 | } 24 | 25 | /// 保存应用配置 26 | pub fn save_app_config(config: &AppConfig) -> Result<(), String> { 27 | let data_dir = get_data_dir()?; 28 | let config_path = data_dir.join(CONFIG_FILE); 29 | 30 | let content = serde_json::to_string_pretty(config) 31 | .map_err(|e| format!("序列化配置失败: {}", e))?; 32 | 33 | fs::write(&config_path, content) 34 | .map_err(|e| format!("保存配置失败: {}", e)) 35 | } 36 | -------------------------------------------------------------------------------- /src-tauri/src/error.rs: -------------------------------------------------------------------------------- 1 | use serde::Serialize; 2 | use thiserror::Error; 3 | 4 | #[derive(Error, Debug)] 5 | pub enum AppError { 6 | #[error("Database error: {0}")] 7 | Database(#[from] rusqlite::Error), 8 | 9 | #[error("Network error: {0}")] 10 | Network(#[from] reqwest::Error), 11 | 12 | #[error("IO error: {0}")] 13 | Io(#[from] std::io::Error), 14 | 15 | #[error("Tauri error: {0}")] 16 | Tauri(#[from] tauri::Error), 17 | 18 | #[error("OAuth error: {0}")] 19 | OAuth(String), 20 | 21 | #[error("Configuration error: {0}")] 22 | Config(String), 23 | 24 | #[error("Account error: {0}")] 25 | Account(String), 26 | 27 | #[error("Unknown error: {0}")] 28 | Unknown(String), 29 | } 30 | 31 | // 实现 Serialize 以便可以作为 Tauri 命令的返回值 32 | impl Serialize for AppError { 33 | fn serialize(&self, serializer: S) -> Result 34 | where 35 | S: serde::Serializer, 36 | { 37 | serializer.serialize_str(self.to_string().as_str()) 38 | } 39 | } 40 | 41 | // 为 Result 实现别名,简化使用 42 | pub type AppResult = Result; 43 | -------------------------------------------------------------------------------- /src/i18n.ts: -------------------------------------------------------------------------------- 1 | 2 | import i18n from 'i18next'; 3 | import { initReactI18next } from 'react-i18next'; 4 | import LanguageDetector from 'i18next-browser-languagedetector'; 5 | 6 | import en from './locales/en.json'; 7 | import zh from './locales/zh.json'; 8 | 9 | i18n 10 | // detect user language 11 | // learn more: https://github.com/i18next/i18next-browser-languagedetector 12 | .use(LanguageDetector) 13 | // pass the i18n instance to react-i18next. 14 | .use(initReactI18next) 15 | // init i18next 16 | // for all options read: https://www.i18next.com/overview/configuration-options 17 | .init({ 18 | resources: { 19 | en: { 20 | translation: en 21 | }, 22 | zh: { 23 | translation: zh 24 | }, 25 | // Handling 'zh-CN' as 'zh' 26 | 'zh-CN': { 27 | translation: zh 28 | } 29 | }, 30 | fallbackLng: 'en', 31 | debug: false, // Set to true for development 32 | 33 | interpolation: { 34 | escapeValue: false, // not needed for react as it escapes by default 35 | } 36 | }); 37 | 38 | export default i18n; 39 | -------------------------------------------------------------------------------- /src-tauri/tauri.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.tauri.app/config/2", 3 | "productName": "Antigravity Tools", 4 | "version": "2.1.2", 5 | "identifier": "com.lbjlaq.antigravity-tools", 6 | "build": { 7 | "beforeDevCommand": "npm run dev", 8 | "devUrl": "http://localhost:1420", 9 | "beforeBuildCommand": "npm run build", 10 | "frontendDist": "../dist" 11 | }, 12 | "app": { 13 | "withGlobalTauri": false, 14 | "windows": [ 15 | { 16 | "title": "Antigravity Tools", 17 | "width": 1024, 18 | "height": 700, 19 | "titleBarStyle": "Overlay", 20 | "hiddenTitle": true, 21 | "transparent": true, 22 | "visible": false 23 | } 24 | ], 25 | "security": { 26 | "csp": "default-src 'self'; img-src 'self' asset: data:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; connect-src ipc: http://ipc.localhost" 27 | } 28 | }, 29 | "bundle": { 30 | "active": true, 31 | "targets": "all", 32 | "icon": [ 33 | "icons/32x32.png", 34 | "icons/128x128.png", 35 | "icons/128x128@2x.png", 36 | "icons/icon.icns", 37 | "icons/icon.ico" 38 | ] 39 | } 40 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "license": "CC-BY-NC-SA-4.0", 3 | "name": "antigravity-tools", 4 | "private": true, 5 | "version": "2.1.2", 6 | "type": "module", 7 | "scripts": { 8 | "dev": "vite", 9 | "build": "tsc && vite build", 10 | "preview": "vite preview", 11 | "tauri": "tauri", 12 | "build:universal": "tauri build --target universal-apple-darwin" 13 | }, 14 | "dependencies": { 15 | "@tauri-apps/api": "^2", 16 | "@tauri-apps/plugin-dialog": "^2.4.2", 17 | "@tauri-apps/plugin-fs": "^2.4.4", 18 | "@tauri-apps/plugin-opener": "^2", 19 | "clsx": "^2.1.1", 20 | "daisyui": "^5.5.13", 21 | "date-fns": "^4.1.0", 22 | "i18next": "^25.7.2", 23 | "i18next-browser-languagedetector": "^8.2.0", 24 | "lucide-react": "^0.561.0", 25 | "react": "^19.1.0", 26 | "react-dom": "^19.1.0", 27 | "react-i18next": "^16.5.0", 28 | "react-router-dom": "^7.10.1", 29 | "recharts": "^3.5.1", 30 | "zustand": "^5.0.9" 31 | }, 32 | "devDependencies": { 33 | "@tauri-apps/cli": "^2", 34 | "@types/react": "^19.1.8", 35 | "@types/react-dom": "^19.1.6", 36 | "@vitejs/plugin-react": "^4.6.0", 37 | "autoprefixer": "^10.4.22", 38 | "postcss": "^8.5.6", 39 | "tailwindcss": "^3.4.19", 40 | "typescript": "~5.8.3", 41 | "vite": "^7.0.4" 42 | } 43 | } -------------------------------------------------------------------------------- /src/components/layout/Layout.tsx: -------------------------------------------------------------------------------- 1 | import { Outlet } from 'react-router-dom'; 2 | import { getCurrentWindow } from '@tauri-apps/api/window'; 3 | import Navbar from './Navbar'; 4 | import BackgroundTaskRunner from '../common/BackgroundTaskRunner'; 5 | import ToastContainer from '../common/ToastContainer'; 6 | 7 | function Layout() { 8 | return ( 9 |
10 | {/* 全局窗口拖拽区域 - 使用 JS 手动触发拖拽,解决 HTML 属性失效问题 */} 11 |
{ 22 | getCurrentWindow().startDragging(); 23 | }} 24 | /> 25 | 26 | 27 | 28 |
29 | 30 |
31 |
32 | ); 33 | } 34 | 35 | export default Layout; 36 | -------------------------------------------------------------------------------- /src-tauri/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "antigravity_tools" 3 | version = "2.0.2" 4 | description = "A Tauri App" 5 | authors = ["you"] 6 | license = "CC-BY-NC-SA-4.0" 7 | edition = "2021" 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | 11 | [lib] 12 | # The `_lib` suffix may seem redundant but it is necessary 13 | # to make the lib name unique and wouldn't conflict with the bin name. 14 | # This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519 15 | name = "antigravity_tools_lib" 16 | crate-type = ["staticlib", "cdylib", "rlib"] 17 | 18 | [build-dependencies] 19 | tauri-build = { version = "2", features = [] } 20 | 21 | [dependencies] 22 | tauri = { version = "2", features = ["tray-icon", "image-png"] } 23 | tauri-plugin-opener = "2" 24 | serde = { version = "1", features = ["derive"] } 25 | serde_json = "1" 26 | uuid = { version = "1.10", features = ["v4", "serde"] } 27 | chrono = "0.4" 28 | dirs = "5.0" 29 | reqwest = { version = "0.12", features = ["json"] } 30 | tracing = "0.1" 31 | tracing-subscriber = "0.3" 32 | rusqlite = { version = "0.32", features = ["bundled"] } 33 | base64 = "0.22" 34 | sysinfo = "0.31" 35 | tokio = { version = "1", features = ["full"] } 36 | url = "2.5.7" 37 | tauri-plugin-dialog = "2.4.2" 38 | tauri-plugin-fs = "2.4.4" 39 | image = "0.25.9" 40 | thiserror = "2.0.17" 41 | 42 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: [ 4 | "./index.html", 5 | "./src/**/*.{js,ts,jsx,tsx}", 6 | ], 7 | darkMode: 'class', 8 | theme: { 9 | extend: {}, 10 | }, 11 | plugins: [require("daisyui")], 12 | daisyui: { 13 | themes: [ 14 | { 15 | light: { 16 | "primary": "#3b82f6", 17 | "secondary": "#64748b", 18 | "accent": "#10b981", 19 | "neutral": "#1f2937", 20 | "base-100": "#ffffff", 21 | "info": "#0ea5e9", 22 | "success": "#10b981", 23 | "warning": "#f59e0b", 24 | "error": "#ef4444", 25 | }, 26 | }, 27 | { 28 | dark: { 29 | "primary": "#3b82f6", 30 | "secondary": "#94a3b8", 31 | "accent": "#10b981", 32 | "neutral": "#1f2937", 33 | "base-100": "#0f172a", // Slate-900 34 | "base-200": "#1e293b", // Slate-800 35 | "base-300": "#334155", // Slate-700 36 | "info": "#0ea5e9", 37 | "success": "#10b981", 38 | "warning": "#f59e0b", 39 | "error": "#ef4444", 40 | }, 41 | }, 42 | ], 43 | darkTheme: "dark", 44 | }, 45 | } 46 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | tags: 5 | - 'v*' 6 | workflow_dispatch: 7 | 8 | jobs: 9 | release: 10 | permissions: 11 | contents: write 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | platform: [macos-latest, ubuntu-22.04, windows-latest] 16 | 17 | runs-on: ${{ matrix.platform }} 18 | 19 | steps: 20 | - name: Checkout repository 21 | uses: actions/checkout@v4 22 | 23 | - name: Install dependencies (Ubuntu only) 24 | if: matrix.platform == 'ubuntu-22.04' 25 | run: | 26 | sudo apt-get update 27 | sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf 28 | 29 | - name: Rust setup 30 | uses: dtolnay/rust-toolchain@stable 31 | with: 32 | # Those targets are only used on macos runners so it's in an `if` to slightly speed up windows and linux builds. 33 | targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }} 34 | 35 | - name: Node.js setup 36 | uses: actions/setup-node@v4 37 | with: 38 | node-version: 20 39 | cache: 'npm' 40 | 41 | - name: Install frontend dependencies 42 | run: npm install 43 | 44 | - name: Build the app 45 | uses: tauri-apps/tauri-action@v0 46 | env: 47 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 48 | with: 49 | tagName: ${{ github.ref_name }} 50 | releaseName: 'Antigravity Tools ${{ github.ref_name }}' 51 | releaseBody: 'See the assets to download this version and install.' 52 | releaseDraft: true 53 | prerelease: false 54 | -------------------------------------------------------------------------------- /src/services/accountService.ts: -------------------------------------------------------------------------------- 1 | import { invoke } from '@tauri-apps/api/core'; 2 | import { Account, QuotaData } from '../types/account'; 3 | 4 | export async function listAccounts(): Promise { 5 | return await invoke('list_accounts'); 6 | } 7 | 8 | export async function getCurrentAccount(): Promise { 9 | return await invoke('get_current_account'); 10 | } 11 | 12 | export async function addAccount(email: string, refreshToken: string): Promise { 13 | return await invoke('add_account', { email, refreshToken }); 14 | } 15 | 16 | export async function deleteAccount(accountId: string): Promise { 17 | return await invoke('delete_account', { accountId }); 18 | } 19 | 20 | export async function switchAccount(accountId: string): Promise { 21 | return await invoke('switch_account', { accountId }); 22 | } 23 | 24 | export async function fetchAccountQuota(accountId: string): Promise { 25 | return await invoke('fetch_account_quota', { accountId }); 26 | } 27 | 28 | export interface RefreshStats { 29 | total: number; 30 | success: number; 31 | failed: number; 32 | details: string[]; 33 | } 34 | 35 | export async function refreshAllQuotas(): Promise { 36 | return await invoke('refresh_all_quotas'); 37 | } 38 | 39 | // OAuth 40 | export async function startOAuthLogin(): Promise { 41 | return await invoke('start_oauth_login'); 42 | } 43 | 44 | export async function cancelOAuthLogin(): Promise { 45 | return await invoke('cancel_oauth_login'); 46 | } 47 | 48 | // 导入 49 | export async function importV1Accounts(): Promise { 50 | return await invoke('import_v1_accounts'); 51 | } 52 | 53 | export async function importFromDb(): Promise { 54 | return await invoke('import_from_db'); 55 | } 56 | -------------------------------------------------------------------------------- /src/stores/useConfigStore.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand'; 2 | import { AppConfig } from '../types/config'; 3 | import * as configService from '../services/configService'; 4 | 5 | interface ConfigState { 6 | config: AppConfig | null; 7 | loading: boolean; 8 | error: string | null; 9 | 10 | // Actions 11 | loadConfig: () => Promise; 12 | saveConfig: (config: AppConfig) => Promise; 13 | updateTheme: (theme: string) => Promise; 14 | updateLanguage: (language: string) => Promise; 15 | } 16 | 17 | export const useConfigStore = create((set, get) => ({ 18 | config: null, 19 | loading: false, 20 | error: null, 21 | 22 | loadConfig: async () => { 23 | set({ loading: true, error: null }); 24 | try { 25 | const config = await configService.loadConfig(); 26 | set({ config, loading: false }); 27 | } catch (error) { 28 | set({ error: String(error), loading: false }); 29 | } 30 | }, 31 | 32 | saveConfig: async (config: AppConfig) => { 33 | set({ loading: true, error: null }); 34 | try { 35 | await configService.saveConfig(config); 36 | set({ config, loading: false }); 37 | } catch (error) { 38 | set({ error: String(error), loading: false }); 39 | throw error; 40 | } 41 | }, 42 | 43 | updateTheme: async (theme: string) => { 44 | const { config } = get(); 45 | if (!config) return; 46 | 47 | const newConfig = { ...config, theme }; 48 | await get().saveConfig(newConfig); 49 | }, 50 | 51 | updateLanguage: async (language: string) => { 52 | const { config } = get(); 53 | if (!config) return; 54 | 55 | const newConfig = { ...config, language }; 56 | await get().saveConfig(newConfig); 57 | }, 58 | })); 59 | -------------------------------------------------------------------------------- /src-tauri/src/models/account.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use super::{token::TokenData, quota::QuotaData}; 3 | 4 | /// 账号数据结构 5 | #[derive(Debug, Clone, Serialize, Deserialize)] 6 | pub struct Account { 7 | pub id: String, 8 | pub email: String, 9 | pub name: Option, 10 | pub token: TokenData, 11 | pub quota: Option, 12 | pub created_at: i64, 13 | pub last_used: i64, 14 | } 15 | 16 | impl Account { 17 | pub fn new(id: String, email: String, token: TokenData) -> Self { 18 | let now = chrono::Utc::now().timestamp(); 19 | Self { 20 | id, 21 | email, 22 | name: None, 23 | token, 24 | quota: None, 25 | created_at: now, 26 | last_used: now, 27 | } 28 | } 29 | 30 | pub fn update_last_used(&mut self) { 31 | self.last_used = chrono::Utc::now().timestamp(); 32 | } 33 | 34 | pub fn update_quota(&mut self, quota: QuotaData) { 35 | self.quota = Some(quota); 36 | } 37 | } 38 | 39 | /// 账号索引数据(accounts.json) 40 | #[derive(Debug, Clone, Serialize, Deserialize)] 41 | pub struct AccountIndex { 42 | pub version: String, 43 | pub accounts: Vec, 44 | pub current_account_id: Option, 45 | } 46 | 47 | /// 账号摘要信息 48 | #[derive(Debug, Clone, Serialize, Deserialize)] 49 | pub struct AccountSummary { 50 | pub id: String, 51 | pub email: String, 52 | pub name: Option, 53 | pub created_at: i64, 54 | pub last_used: i64, 55 | } 56 | 57 | impl AccountIndex { 58 | pub fn new() -> Self { 59 | Self { 60 | version: "2.0".to_string(), 61 | accounts: Vec::new(), 62 | current_account_id: None, 63 | } 64 | } 65 | } 66 | 67 | impl Default for AccountIndex { 68 | fn default() -> Self { 69 | Self::new() 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src-tauri/output_check_final.txt: -------------------------------------------------------------------------------- 1 |  Checking antigravity_tools v0.1.0 (/Users/lbjlaq/Desktop/antigravity_tauri/src-tauri) 2 |  Building [=======================> ] 615/617: antigravity_tools warning: unused variable: `name` 3 |  --> src/modules/process.rs:18:13 4 |  | 5 | 18 |  let name = process.name().to_string_los... 6 |  | ^^^^ help: if this is intentional, prefix it with an underscore: `_name` 7 |  | 8 |  = note: `#[warn(unused_variables)]` (part of `#[warn(unused)]`) on by default 9 | 10 |  Building [=======================> ] 615/617: antigravity_tools warning: unused variable: `name` 11 |  --> src/modules/process.rs:188:13 12 |  | 13 | 188 |  let name = process.name().to_string_lo... 14 |  | ^^^^ help: if this is intentional, prefix it with an underscore: `_name` 15 | 16 |  Building [=======================> ] 615/617: antigravity_tools warning: `antigravity_tools` (lib) generated 2 warnings 17 |  Building [=======================> ] 616/617: antigravity_tools(bin)  Finished ]8;;https://doc.rust-lang.org/cargo/reference/profiles.html#default-profiles\`dev` profile [unoptimized + debuginfo]]8;;\ target(s) in 1.49s 18 | -------------------------------------------------------------------------------- /src-tauri/output_check_owned.txt: -------------------------------------------------------------------------------- 1 |  Checking antigravity_tools v0.1.0 (/Users/lbjlaq/Desktop/antigravity_tauri/src-tauri) 2 |  Building [=======================> ] 615/617: antigravity_tools warning: unused variable: `name` 3 |  --> src/modules/process.rs:18:13 4 |  | 5 | 18 |  let name = process.name().to_string_los... 6 |  | ^^^^ help: if this is intentional, prefix it with an underscore: `_name` 7 |  | 8 |  = note: `#[warn(unused_variables)]` (part of `#[warn(unused)]`) on by default 9 | 10 |  Building [=======================> ] 615/617: antigravity_tools warning: unused variable: `name` 11 |  --> src/modules/process.rs:188:13 12 |  | 13 | 188 |  let name = process.name().to_string_lo... 14 |  | ^^^^ help: if this is intentional, prefix it with an underscore: `_name` 15 | 16 |  Building [=======================> ] 615/617: antigravity_tools warning: `antigravity_tools` (lib) generated 2 warnings 17 |  Building [=======================> ] 616/617: antigravity_tools(bin)  Finished ]8;;https://doc.rust-lang.org/cargo/reference/profiles.html#default-profiles\`dev` profile [unoptimized + debuginfo]]8;;\ target(s) in 1.52s 18 | -------------------------------------------------------------------------------- /src/components/common/ToastContainer.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useCallback, useEffect } from 'react'; 2 | import { createPortal } from 'react-dom'; 3 | import Toast, { ToastType } from './Toast'; 4 | 5 | export interface ToastItem { 6 | id: string; 7 | message: string; 8 | type: ToastType; 9 | duration?: number; 10 | } 11 | 12 | let toastCounter = 0; 13 | let addToastExternal: ((message: string, type: ToastType, duration?: number) => void) | null = null; 14 | 15 | export const showToast = (message: string, type: ToastType = 'info', duration: number = 3000) => { 16 | if (addToastExternal) { 17 | addToastExternal(message, type, duration); 18 | } else { 19 | console.warn('ToastContainer not mounted'); 20 | } 21 | }; 22 | 23 | const ToastContainer = () => { 24 | const [toasts, setToasts] = useState([]); 25 | 26 | const addToast = useCallback((message: string, type: ToastType, duration?: number) => { 27 | const id = `toast-${Date.now()}-${toastCounter++}`; 28 | setToasts(prev => [...prev, { id, message, type, duration }]); 29 | }, []); 30 | 31 | const removeToast = useCallback((id: string) => { 32 | setToasts(prev => prev.filter(t => t.id !== id)); 33 | }, []); 34 | 35 | useEffect(() => { 36 | addToastExternal = addToast; 37 | return () => { 38 | addToastExternal = null; 39 | }; 40 | }, [addToast]); 41 | 42 | return createPortal( 43 |
44 |
45 | {toasts.map(toast => ( 46 | 51 | ))} 52 |
53 |
, 54 | document.body 55 | ); 56 | }; 57 | 58 | export default ToastContainer; 59 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Antigravity Tools 9 | 34 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 |
56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /src/utils/format.ts: -------------------------------------------------------------------------------- 1 | import { formatDistanceToNow } from 'date-fns'; 2 | import { zhCN, enUS } from 'date-fns/locale'; 3 | 4 | export function formatRelativeTime(timestamp: number, language: string = 'zh-CN'): string { 5 | const locale = language === 'zh-CN' ? zhCN : enUS; 6 | return formatDistanceToNow(new Date(timestamp * 1000), { 7 | addSuffix: true, 8 | locale, 9 | }); 10 | } 11 | 12 | export function formatBytes(bytes: number): string { 13 | if (bytes === 0) return '0 Bytes'; 14 | 15 | const k = 1024; 16 | const sizes = ['Bytes', 'KB', 'MB', 'GB']; 17 | const i = Math.floor(Math.log(bytes) / Math.log(k)); 18 | 19 | return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]; 20 | } 21 | 22 | export function getQuotaColor(percentage: number): string { 23 | if (percentage >= 50) return 'success'; 24 | if (percentage >= 20) return 'warning'; 25 | return 'error'; 26 | } 27 | 28 | export function formatTimeRemaining(dateStr: string): string { 29 | const targetDate = new Date(dateStr); 30 | const now = new Date(); 31 | const diffMs = targetDate.getTime() - now.getTime(); 32 | 33 | if (diffMs <= 0) return '0h 0m'; 34 | 35 | const diffHrs = Math.floor(diffMs / (1000 * 60 * 60)); 36 | const diffMins = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60)); 37 | 38 | if (diffHrs >= 24) { 39 | const diffDays = Math.floor(diffHrs / 24); 40 | const remainingHrs = diffHrs % 24; 41 | return `${diffDays}d ${remainingHrs}h`; 42 | } 43 | 44 | return `${diffHrs}h ${diffMins}m`; 45 | } 46 | 47 | export function formatDate(timestamp: string | number | undefined | null): string | null { 48 | if (!timestamp) return null; 49 | const date = typeof timestamp === 'number' 50 | ? new Date(timestamp * 1000) 51 | : new Date(timestamp); 52 | 53 | if (isNaN(date.getTime())) return null; 54 | 55 | return date.toLocaleString(undefined, { 56 | year: 'numeric', 57 | month: '2-digit', 58 | day: '2-digit', 59 | hour: '2-digit', 60 | minute: '2-digit', 61 | second: '2-digit', 62 | hour12: false 63 | }); 64 | } 65 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | /* 禁止过度滚动和橡皮筋效果 */ 6 | html, 7 | body { 8 | overscroll-behavior: none; 9 | height: 100%; 10 | overflow: hidden; 11 | margin: 0; 12 | padding: 0; 13 | border: none; 14 | } 15 | 16 | html { 17 | background-color: #FAFBFC; 18 | } 19 | 20 | html.dark { 21 | background-color: #1d232a; 22 | } 23 | 24 | /* 全局样式 */ 25 | body { 26 | margin: 0; 27 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 28 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 29 | sans-serif; 30 | -webkit-font-smoothing: antialiased; 31 | -moz-osx-font-smoothing: grayscale; 32 | background-color: #FAFBFC; 33 | } 34 | 35 | /* Dark mode override for body strictly */ 36 | .dark body { 37 | background-color: #1d232a; 38 | /* matches base-300 commonly used */ 39 | } 40 | 41 | #root { 42 | width: 100%; 43 | height: 100%; 44 | overflow-y: auto; 45 | overscroll-behavior: none; 46 | } 47 | 48 | /* 移除默认的 tap 高亮 */ 49 | * { 50 | -webkit-tap-highlight-color: transparent; 51 | } 52 | 53 | /* 只移除链接的默认下划线,不强制颜色 */ 54 | a { 55 | text-decoration: none; 56 | } 57 | 58 | /* 滚动条优化 - 彻底隐藏但保留功能 */ 59 | ::-webkit-scrollbar { 60 | width: 0px; 61 | background: transparent; 62 | } 63 | 64 | ::-webkit-scrollbar-track { 65 | background-color: transparent; 66 | } 67 | 68 | ::-webkit-scrollbar-thumb { 69 | background-color: rgba(0, 0, 0, 0.1); 70 | border-radius: 99px; 71 | border: 3px solid transparent; 72 | background-clip: content-box; 73 | transition: background-color 0.2s; 74 | } 75 | 76 | ::-webkit-scrollbar-thumb:hover { 77 | background-color: rgba(0, 0, 0, 0.3); 78 | } 79 | 80 | /* View Transitions API 主题切换动画 */ 81 | ::view-transition-old(root), 82 | ::view-transition-new(root) { 83 | animation: none; 84 | mix-blend-mode: normal; 85 | } 86 | 87 | ::view-transition-old(root) { 88 | z-index: 1; 89 | } 90 | 91 | ::view-transition-new(root) { 92 | z-index: 9999; 93 | } 94 | 95 | .dark::view-transition-old(root) { 96 | z-index: 9999; 97 | } 98 | 99 | .dark::view-transition-new(root) { 100 | z-index: 1; 101 | } -------------------------------------------------------------------------------- /src-tauri/src/modules/i18n.rs: -------------------------------------------------------------------------------- 1 | use serde_json::Value; 2 | use std::collections::HashMap; 3 | 4 | /// 托盘文本结构 5 | #[derive(Debug, Clone)] 6 | pub struct TrayTexts { 7 | pub current: String, 8 | pub quota: String, 9 | pub switch_next: String, 10 | pub refresh_current: String, 11 | pub show_window: String, 12 | pub quit: String, 13 | pub no_account: String, 14 | pub unknown_quota: String, 15 | pub forbidden: String, 16 | } 17 | 18 | /// 从 JSON 加载翻译 19 | fn load_translations(lang: &str) -> HashMap { 20 | let json_content = match lang { 21 | "en" | "en-US" => include_str!("../../../src/locales/en.json"), 22 | _ => include_str!("../../../src/locales/zh.json"), 23 | }; 24 | 25 | let v: Value = serde_json::from_str(json_content) 26 | .unwrap_or_else(|_| serde_json::json!({})); 27 | 28 | let mut map = HashMap::new(); 29 | 30 | if let Some(tray) = v.get("tray").and_then(|t| t.as_object()) { 31 | for (key, value) in tray { 32 | if let Some(s) = value.as_str() { 33 | map.insert(key.clone(), s.to_string()); 34 | } 35 | } 36 | } 37 | 38 | map 39 | } 40 | 41 | /// 获取托盘文本(根据语言) 42 | pub fn get_tray_texts(lang: &str) -> TrayTexts { 43 | let t = load_translations(lang); 44 | 45 | TrayTexts { 46 | current: t.get("current").cloned().unwrap_or_else(|| "Current".to_string()), 47 | quota: t.get("quota").cloned().unwrap_or_else(|| "Quota".to_string()), 48 | switch_next: t.get("switch_next").cloned().unwrap_or_else(|| "Switch to Next Account".to_string()), 49 | refresh_current: t.get("refresh_current").cloned().unwrap_or_else(|| "Refresh Current Quota".to_string()), 50 | show_window: t.get("show_window").cloned().unwrap_or_else(|| "Show Main Window".to_string()), 51 | quit: t.get("quit").cloned().unwrap_or_else(|| "Quit Application".to_string()), 52 | no_account: t.get("no_account").cloned().unwrap_or_else(|| "No Account".to_string()), 53 | unknown_quota: t.get("unknown_quota").cloned().unwrap_or_else(|| "Unknown".to_string()), 54 | forbidden: t.get("forbidden").cloned().unwrap_or_else(|| "Account Forbidden".to_string()), 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/components/accounts/AccountGrid.tsx: -------------------------------------------------------------------------------- 1 | import { Account } from '../../types/account'; 2 | import AccountCard from './AccountCard'; 3 | 4 | interface AccountGridProps { 5 | accounts: Account[]; 6 | selectedIds: Set; 7 | refreshingIds: Set; 8 | onToggleSelect: (id: string) => void; 9 | currentAccountId: string | null; 10 | switchingAccountId: string | null; 11 | onSwitch: (accountId: string) => void; 12 | onRefresh: (accountId: string) => void; 13 | onViewDetails: (accountId: string) => void; 14 | onExport: (accountId: string) => void; 15 | onDelete: (accountId: string) => void; 16 | } 17 | 18 | function AccountGrid({ accounts, selectedIds, refreshingIds, onToggleSelect, currentAccountId, switchingAccountId, onSwitch, onRefresh, onViewDetails, onExport, onDelete }: AccountGridProps) { 19 | if (accounts.length === 0) { 20 | return ( 21 |
22 |

暂无账号

23 |

点击上方"添加账号"按钮添加第一个账号

24 |
25 | ); 26 | } 27 | 28 | return ( 29 |
30 | {accounts.map((account) => ( 31 | onToggleSelect(account.id)} 37 | isCurrent={account.id === currentAccountId} 38 | isSwitching={account.id === switchingAccountId} 39 | onSwitch={() => onSwitch(account.id)} 40 | onRefresh={() => onRefresh(account.id)} 41 | onViewDetails={() => onViewDetails(account.id)} 42 | onExport={() => onExport(account.id)} 43 | onDelete={() => onDelete(account.id)} 44 | /> 45 | ))} 46 |
47 | ); 48 | } 49 | 50 | export default AccountGrid; 51 | -------------------------------------------------------------------------------- /src-tauri/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod models; 2 | mod modules; 3 | mod commands; 4 | mod utils; 5 | pub mod error; 6 | 7 | use modules::logger; 8 | 9 | // 测试命令 10 | #[tauri::command] 11 | fn greet(name: &str) -> String { 12 | format!("Hello, {}! You've been greeted from Rust!", name) 13 | } 14 | 15 | #[cfg_attr(mobile, tauri::mobile_entry_point)] 16 | pub fn run() { 17 | // 初始化日志 18 | logger::init_logger(); 19 | 20 | tauri::Builder::default() 21 | .plugin(tauri_plugin_dialog::init()) 22 | .plugin(tauri_plugin_fs::init()) 23 | .plugin(tauri_plugin_opener::init()) 24 | .setup(|app| { 25 | modules::tray::create_tray(app.handle())?; 26 | Ok(()) 27 | }) 28 | .on_window_event(|window, event| { 29 | if let tauri::WindowEvent::CloseRequested { api, .. } = event { 30 | let _ = window.hide(); 31 | #[cfg(target_os = "macos")] 32 | { 33 | use tauri::Manager; 34 | window.app_handle().set_activation_policy(tauri::ActivationPolicy::Accessory).unwrap_or(()); 35 | } 36 | api.prevent_close(); 37 | } 38 | }) 39 | .invoke_handler(tauri::generate_handler![ 40 | greet, 41 | // 账号管理命令 42 | commands::list_accounts, 43 | commands::add_account, 44 | commands::delete_account, 45 | commands::switch_account, 46 | commands::get_current_account, 47 | // 配额命令 48 | commands::fetch_account_quota, 49 | commands::refresh_all_quotas, 50 | // 配置命令 51 | commands::load_config, 52 | commands::save_config, 53 | // 新增命令 54 | commands::start_oauth_login, 55 | commands::cancel_oauth_login, 56 | commands::import_v1_accounts, 57 | commands::import_from_db, 58 | commands::save_text_file, 59 | commands::clear_log_cache, 60 | commands::open_data_folder, 61 | commands::get_data_dir_path, 62 | commands::show_main_window, 63 | commands::get_antigravity_path, 64 | ]) 65 | .run(tauri::generate_context!()) 66 | .expect("error while running tauri application"); 67 | } 68 | -------------------------------------------------------------------------------- /src-tauri/src/modules/db.rs: -------------------------------------------------------------------------------- 1 | use rusqlite::Connection; 2 | use base64::{Engine as _, engine::general_purpose}; 3 | use std::path::PathBuf; 4 | use crate::utils::protobuf; 5 | 6 | /// 获取 Antigravity 数据库路径(跨平台) 7 | pub fn get_db_path() -> Result { 8 | #[cfg(target_os = "macos")] 9 | { 10 | let home = dirs::home_dir().ok_or("无法获取 Home 目录")?; 11 | Ok(home.join("Library/Application Support/Antigravity/User/globalStorage/state.vscdb")) 12 | } 13 | 14 | #[cfg(target_os = "windows")] 15 | { 16 | let appdata = std::env::var("APPDATA") 17 | .map_err(|_| "无法获取 APPDATA 环境变量".to_string())?; 18 | Ok(PathBuf::from(appdata).join("Antigravity\\User\\globalStorage\\state.vscdb")) 19 | } 20 | 21 | #[cfg(target_os = "linux")] 22 | { 23 | let home = dirs::home_dir().ok_or("无法获取 Home 目录")?; 24 | Ok(home.join(".config/Antigravity/User/globalStorage/state.vscdb")) 25 | } 26 | } 27 | 28 | /// 注入 Token 到数据库 29 | pub fn inject_token( 30 | db_path: &PathBuf, 31 | access_token: &str, 32 | refresh_token: &str, 33 | expiry: i64, 34 | ) -> Result { 35 | // 1. 打开数据库 36 | let conn = Connection::open(db_path) 37 | .map_err(|e| format!("打开数据库失败: {}", e))?; 38 | 39 | // 2. 读取当前数据 40 | let current_data: String = conn 41 | .query_row( 42 | "SELECT value FROM ItemTable WHERE key = ?", 43 | ["jetskiStateSync.agentManagerInitState"], 44 | |row| row.get(0), 45 | ) 46 | .map_err(|e| format!("读取数据失败: {}", e))?; 47 | 48 | // 3. Base64 解码 49 | let blob = general_purpose::STANDARD 50 | .decode(¤t_data) 51 | .map_err(|e| format!("Base64 解码失败: {}", e))?; 52 | 53 | // 4. 移除旧 Field 6 54 | let clean_data = protobuf::remove_field(&blob, 6)?; 55 | 56 | // 5. 创建新 Field 6 57 | let new_field = protobuf::create_oauth_field(access_token, refresh_token, expiry); 58 | 59 | // 6. 合并数据 60 | let final_data = [clean_data, new_field].concat(); 61 | let final_b64 = general_purpose::STANDARD.encode(&final_data); 62 | 63 | // 7. 写入数据库 64 | conn.execute( 65 | "UPDATE ItemTable SET value = ? WHERE key = ?", 66 | [&final_b64, "jetskiStateSync.agentManagerInitState"], 67 | ) 68 | .map_err(|e| format!("写入数据失败: {}", e))?; 69 | 70 | Ok(format!("Token 注入成功!\n数据库: {:?}", db_path)) 71 | } 72 | -------------------------------------------------------------------------------- /src-tauri/src/modules/logger.rs: -------------------------------------------------------------------------------- 1 | use tracing::{info, warn, error}; 2 | use tracing_subscriber; 3 | use std::fs; 4 | use std::path::PathBuf; 5 | use crate::modules::account::get_data_dir; 6 | 7 | pub fn get_log_dir() -> Result { 8 | let data_dir = get_data_dir()?; 9 | let log_dir = data_dir.join("logs"); 10 | 11 | if !log_dir.exists() { 12 | fs::create_dir_all(&log_dir).map_err(|e| format!("创建日志目录失败: {}", e))?; 13 | } 14 | 15 | Ok(log_dir) 16 | } 17 | 18 | /// 初始化日志系统 19 | pub fn init_logger() { 20 | tracing_subscriber::fmt() 21 | .with_target(false) 22 | .with_thread_ids(false) 23 | .with_level(true) 24 | .init(); 25 | 26 | // 简单的文件日志模拟 (因缺少 tracing-appender) 27 | if let Ok(log_dir) = get_log_dir() { 28 | let log_file = log_dir.join("app.log"); 29 | let _ = fs::write(log_file, format!("Log init at {}\n", chrono::Local::now())); 30 | } 31 | 32 | info!("日志系统已初始化"); 33 | } 34 | 35 | /// 清理日志缓存 36 | pub fn clear_logs() -> Result<(), String> { 37 | let log_dir = get_log_dir()?; 38 | if log_dir.exists() { 39 | fs::remove_dir_all(&log_dir).map_err(|e| format!("清理日志目录失败: {}", e))?; 40 | fs::create_dir_all(&log_dir).map_err(|e| format!("重建日志目录失败: {}", e))?; 41 | 42 | // 重建后立即写入一条初始日志,确保文件存在 43 | let log_file = log_dir.join("app.log"); 44 | let _ = fs::write(log_file, format!("Log cleared at {}\n", chrono::Local::now())); 45 | } 46 | Ok(()) 47 | } 48 | 49 | fn append_log(level: &str, message: &str) { 50 | if let Ok(log_dir) = get_log_dir() { 51 | let log_file = log_dir.join("app.log"); 52 | // 使用 append 模式打开文件,如果文件不存在则创建 53 | if let Ok(mut file) = fs::OpenOptions::new().create(true).append(true).open(log_file) { 54 | use std::io::Write; 55 | let time = chrono::Local::now().format("%Y-%m-%d %H:%M:%S"); 56 | let _ = writeln!(file, "[{}] [{}] {}", time, level, message); 57 | } 58 | } 59 | } 60 | 61 | /// 记录信息日志 62 | pub fn log_info(message: &str) { 63 | info!("{}", message); 64 | append_log("INFO", message); 65 | } 66 | 67 | /// 记录警告日志 68 | pub fn log_warn(message: &str) { 69 | warn!("{}", message); 70 | append_log("WARN", message); 71 | } 72 | 73 | /// 记录错误日志 74 | pub fn log_error(message: &str) { 75 | error!("{}", message); 76 | append_log("ERROR", message); 77 | } 78 | -------------------------------------------------------------------------------- /src-tauri/output_check.txt: -------------------------------------------------------------------------------- 1 |  Checking tauri-plugin-fs v2.4.4 2 |  Compiling antigravity_tools v0.1.0 (/Users/lbjlaq/Desktop/antigravity_tauri/src-tauri) 3 |  Checking tauri-plugin-opener v2.5.2 4 |  Building [=======================> ] 544/550: tauri-plugin-opener, taur...  Building [=======================> ] 545/550: tauri-plugin-fs, antigrav...  Checking tauri-plugin-dialog v2.4.2 5 |  Building [=======================> ] 546/550: tauri-plugin-dialog, anti...  Building [=======================> ] 547/550: tauri-plugin-dialog  Building [=======================> ] 548/550: antigravity_tools warning: unused variable: `name` 6 |  --> src/modules/process.rs:18:13 7 |  | 8 | 18 |  let name = process.name().to_string_los... 9 |  | ^^^^ help: if this is intentional, prefix it with an underscore: `_name` 10 |  | 11 |  = note: `#[warn(unused_variables)]` (part of `#[warn(unused)]`) on by default 12 | 13 |  Building [=======================> ] 548/550: antigravity_tools warning: unused variable: `name` 14 |  --> src/modules/process.rs:188:13 15 |  | 16 | 188 |  let name = process.name().to_string_lo... 17 |  | ^^^^ help: if this is intentional, prefix it with an underscore: `_name` 18 | 19 |  Building [=======================> ] 548/550: antigravity_tools warning: `antigravity_tools` (lib) generated 2 warnings 20 |  Building [=======================> ] 549/550: antigravity_tools(bin)  Finished ]8;;https://doc.rust-lang.org/cargo/reference/profiles.html#default-profiles\`dev` profile [unoptimized + debuginfo]]8;;\ target(s) in 1.68s 21 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { createBrowserRouter, RouterProvider } from 'react-router-dom'; 2 | 3 | import Layout from './components/layout/Layout'; 4 | import Dashboard from './pages/Dashboard'; 5 | import Accounts from './pages/Accounts'; 6 | import Settings from './pages/Settings'; 7 | import ThemeManager from './components/common/ThemeManager'; 8 | import { useEffect } from 'react'; 9 | import { useConfigStore } from './stores/useConfigStore'; 10 | import { useAccountStore } from './stores/useAccountStore'; 11 | import { useTranslation } from 'react-i18next'; 12 | import { listen } from '@tauri-apps/api/event'; 13 | 14 | const router = createBrowserRouter([ 15 | { 16 | path: '/', 17 | element: , 18 | children: [ 19 | { 20 | index: true, 21 | element: , 22 | }, 23 | { 24 | path: 'accounts', 25 | element: , 26 | }, 27 | { 28 | path: 'settings', 29 | element: , 30 | }, 31 | ], 32 | }, 33 | ]); 34 | 35 | function App() { 36 | const { config, loadConfig } = useConfigStore(); 37 | const { fetchCurrentAccount, fetchAccounts } = useAccountStore(); 38 | const { i18n } = useTranslation(); 39 | 40 | useEffect(() => { 41 | loadConfig(); 42 | }, [loadConfig]); 43 | 44 | // Sync language from config 45 | useEffect(() => { 46 | if (config?.language) { 47 | i18n.changeLanguage(config.language); 48 | } 49 | }, [config?.language, i18n]); 50 | 51 | // Listen for tray events 52 | useEffect(() => { 53 | const unlistenPromises: Promise<() => void>[] = []; 54 | 55 | // 监听托盘切换账号事件 56 | unlistenPromises.push( 57 | listen('tray://account-switched', () => { 58 | console.log('[App] Tray account switched, refreshing...'); 59 | fetchCurrentAccount(); 60 | fetchAccounts(); 61 | }) 62 | ); 63 | 64 | // 监听托盘刷新事件 65 | unlistenPromises.push( 66 | listen('tray://refresh-current', () => { 67 | console.log('[App] Tray refresh triggered, refreshing...'); 68 | fetchCurrentAccount(); 69 | fetchAccounts(); 70 | }) 71 | ); 72 | 73 | // Cleanup 74 | return () => { 75 | Promise.all(unlistenPromises).then(unlisteners => { 76 | unlisteners.forEach(unlisten => unlisten()); 77 | }); 78 | }; 79 | }, [fetchCurrentAccount, fetchAccounts]); 80 | 81 | return ( 82 | <> 83 | 84 | 85 | 86 | ); 87 | } 88 | 89 | export default App; 90 | -------------------------------------------------------------------------------- /public/tauri.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/components/common/ThemeManager.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { useEffect } from 'react'; 3 | import { useConfigStore } from '../../stores/useConfigStore'; 4 | import { getCurrentWindow } from '@tauri-apps/api/window'; 5 | 6 | export default function ThemeManager() { 7 | const { config, loadConfig } = useConfigStore(); 8 | 9 | // Load config on mount 10 | useEffect(() => { 11 | const init = async () => { 12 | await loadConfig(); 13 | // Show window after a short delay to ensure React has painted 14 | setTimeout(async () => { 15 | await getCurrentWindow().show(); 16 | }, 100); 17 | }; 18 | init(); 19 | }, [loadConfig]); 20 | 21 | // Apply theme when config changes 22 | useEffect(() => { 23 | if (!config) return; 24 | 25 | const applyTheme = async (theme: string) => { 26 | const root = document.documentElement; 27 | const isDark = theme === 'dark'; 28 | 29 | // Set Tauri window background color 30 | try { 31 | const bgColor = isDark ? '#1d232a' : '#FAFBFC'; 32 | await getCurrentWindow().setBackgroundColor(bgColor); 33 | } catch (e) { 34 | console.error('Failed to set window background color:', e); 35 | } 36 | 37 | // Set DaisyUI theme 38 | root.setAttribute('data-theme', theme); 39 | 40 | // Set inline style for immediate visual feedback 41 | root.style.backgroundColor = isDark ? '#1d232a' : '#FAFBFC'; 42 | 43 | // Set Tailwind dark mode class 44 | if (isDark) { 45 | root.classList.add('dark'); 46 | } else { 47 | root.classList.remove('dark'); 48 | } 49 | }; 50 | 51 | const theme = config.theme || 'system'; 52 | 53 | // Sync to localStorage for early boot check 54 | localStorage.setItem('app-theme-preference', theme); 55 | 56 | if (theme === 'system') { 57 | const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); 58 | 59 | const handleSystemChange = (e: MediaQueryListEvent | MediaQueryList) => { 60 | const systemTheme = e.matches ? 'dark' : 'light'; 61 | applyTheme(systemTheme); 62 | }; 63 | 64 | // Initial alignment 65 | handleSystemChange(mediaQuery); 66 | 67 | // Listen for changes 68 | mediaQuery.addEventListener('change', handleSystemChange); 69 | return () => mediaQuery.removeEventListener('change', handleSystemChange); 70 | } else { 71 | applyTheme(theme); 72 | } 73 | }, [config?.theme]); 74 | 75 | return null; // This component handles side effects only 76 | } 77 | -------------------------------------------------------------------------------- /src/components/common/Toast.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { CheckCircle, XCircle, Info, AlertTriangle, X } from 'lucide-react'; 3 | 4 | export type ToastType = 'success' | 'error' | 'info' | 'warning'; 5 | 6 | export interface ToastProps { 7 | id: string; 8 | message: string; 9 | type: ToastType; 10 | duration?: number; 11 | onClose: (id: string) => void; 12 | } 13 | 14 | const Toast = ({ id, message, type, duration = 3000, onClose }: ToastProps) => { 15 | const [isVisible, setIsVisible] = useState(false); 16 | 17 | useEffect(() => { 18 | // Exciting entrance 19 | requestAnimationFrame(() => setIsVisible(true)); 20 | 21 | if (duration > 0) { 22 | const timer = setTimeout(() => { 23 | setIsVisible(false); 24 | setTimeout(() => onClose(id), 300); // Wait for transition 25 | }, duration); 26 | return () => clearTimeout(timer); 27 | } 28 | }, [duration, id, onClose]); 29 | 30 | const getIcon = () => { 31 | switch (type) { 32 | case 'success': return ; 33 | case 'error': return ; 34 | case 'warning': return ; 35 | case 'info': default: return ; 36 | } 37 | }; 38 | 39 | const getStyles = () => { 40 | switch (type) { 41 | case 'success': return 'border-green-100 dark:border-green-900/30 bg-white dark:bg-base-100'; 42 | case 'error': return 'border-red-100 dark:border-red-900/30 bg-white dark:bg-base-100'; 43 | case 'warning': return 'border-yellow-100 dark:border-yellow-900/30 bg-white dark:bg-base-100'; 44 | case 'info': default: return 'border-blue-100 dark:border-blue-900/30 bg-white dark:bg-base-100'; 45 | } 46 | }; 47 | 48 | return ( 49 |
53 | {getIcon()} 54 |

{message}

55 | 61 |
62 | ); 63 | }; 64 | 65 | export default Toast; 66 | -------------------------------------------------------------------------------- /src-tauri/output_check_final_4.txt: -------------------------------------------------------------------------------- 1 |  Checking antigravity_tools v0.1.0 (/Users/lbjlaq/Desktop/antigravity_tauri/src-tauri) 2 |  Building [=======================> ] 615/617: antigravity_tools warning: unused variable: `name` 3 |  --> src/modules/process.rs:18:13 4 |  | 5 | 18 |  let name = process.name().to_string_los... 6 |  | ^^^^ help: if this is intentional, prefix it with an underscore: `_name` 7 |  | 8 |  = note: `#[warn(unused_variables)]` (part of `#[warn(unused)]`) on by default 9 | 10 |  Building [=======================> ] 615/617: antigravity_tools warning: unused variable: `name` 11 |  --> src/modules/process.rs:188:13 12 |  | 13 | 188 |  let name = process.name().to_string_lo... 14 |  | ^^^^ help: if this is intentional, prefix it with an underscore: `_name` 15 | 16 |  Building [=======================> ] 615/617: antigravity_tools warning: variable does not need to be mutable 17 |  --> src/modules/tray.rs:155:24 18 |  | 19 | 155 | ...let Ok(mut account) = modules::load_account... 20 |  | ----^^^^^^^ 21 |  | | 22 |  | help: remove this `mut` 23 |  | 24 |  = note: `#[warn(unused_mut)]` (part of `#[warn(unused)]`) on by default 25 | 26 |  Building [=======================> ] 615/617: antigravity_tools warning: `antigravity_tools` (lib) generated 3 warnings (run `cargo fix --lib -p antigravity_tools` to apply 1 suggestion) 27 |  Building [=======================> ] 616/617: antigravity_tools(bin)  Finished ]8;;https://doc.rust-lang.org/cargo/reference/profiles.html#default-profiles\`dev` profile [unoptimized + debuginfo]]8;;\ target(s) in 1.44s 28 | -------------------------------------------------------------------------------- /src/components/common/BackgroundTaskRunner.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | import { useConfigStore } from '../../stores/useConfigStore'; 3 | import { useAccountStore } from '../../stores/useAccountStore'; 4 | 5 | function BackgroundTaskRunner() { 6 | const { config } = useConfigStore(); 7 | const { refreshAllQuotas, fetchCurrentAccount } = useAccountStore(); 8 | 9 | // Use refs to track previous state to detect "off -> on" transitions 10 | const prevAutoRefreshRef = useRef(false); 11 | const prevAutoSyncRef = useRef(false); 12 | 13 | // Auto Refresh Quota Effect 14 | useEffect(() => { 15 | if (!config) return; 16 | 17 | let intervalId: ReturnType | null = null; 18 | const { auto_refresh, refresh_interval } = config; 19 | 20 | // Check if we just turned it on 21 | if (auto_refresh && !prevAutoRefreshRef.current) { 22 | console.log('[BackgroundTask] Auto-refresh enabled, executing immediately...'); 23 | refreshAllQuotas(); 24 | } 25 | prevAutoRefreshRef.current = auto_refresh; 26 | 27 | if (auto_refresh && refresh_interval > 0) { 28 | console.log(`[BackgroundTask] Starting auto-refresh quota timer: ${refresh_interval} mins`); 29 | intervalId = setInterval(() => { 30 | console.log('[BackgroundTask] Auto-refreshing all quotas...'); 31 | refreshAllQuotas(); 32 | }, refresh_interval * 60 * 1000); 33 | } 34 | 35 | return () => { 36 | if (intervalId) { 37 | console.log('[BackgroundTask] Clearing auto-refresh timer'); 38 | clearInterval(intervalId); 39 | } 40 | }; 41 | }, [config?.auto_refresh, config?.refresh_interval]); 42 | 43 | // Auto Sync Current Account Effect 44 | useEffect(() => { 45 | if (!config) return; 46 | 47 | let intervalId: ReturnType | null = null; 48 | const { auto_sync, sync_interval } = config; 49 | 50 | // Check if we just turned it on 51 | if (auto_sync && !prevAutoSyncRef.current) { 52 | console.log('[BackgroundTask] Auto-sync enabled, executing immediately...'); 53 | fetchCurrentAccount(); 54 | } 55 | prevAutoSyncRef.current = auto_sync; 56 | 57 | if (auto_sync && sync_interval > 0) { 58 | console.log(`[BackgroundTask] Starting auto-sync account timer: ${sync_interval} seconds`); 59 | intervalId = setInterval(() => { 60 | console.log('[BackgroundTask] Auto-syncing current account...'); 61 | fetchCurrentAccount(); 62 | }, sync_interval * 1000); 63 | } 64 | 65 | return () => { 66 | if (intervalId) { 67 | console.log('[BackgroundTask] Clearing auto-sync timer'); 68 | clearInterval(intervalId); 69 | } 70 | }; 71 | }, [config?.auto_sync, config?.sync_interval]); 72 | 73 | // Render nothing 74 | return null; 75 | } 76 | 77 | export default BackgroundTaskRunner; 78 | -------------------------------------------------------------------------------- /src-tauri/output_check_final_3.txt: -------------------------------------------------------------------------------- 1 |  Checking antigravity_tools v0.1.0 (/Users/lbjlaq/Desktop/antigravity_tauri/src-tauri) 2 |  Building [=======================> ] 615/617: antigravity_tools warning: unused variable: `name` 3 |  --> src/modules/process.rs:18:13 4 |  | 5 | 18 |  let name = process.name().to_string_los... 6 |  | ^^^^ help: if this is intentional, prefix it with an underscore: `_name` 7 |  | 8 |  = note: `#[warn(unused_variables)]` (part of `#[warn(unused)]`) on by default 9 | 10 |  Building [=======================> ] 615/617: antigravity_tools warning: unused variable: `name` 11 |  --> src/modules/process.rs:188:13 12 |  | 13 | 188 |  let name = process.name().to_string_lo... 14 |  | ^^^^ help: if this is intentional, prefix it with an underscore: `_name` 15 | 16 |  Building [=======================> ] 615/617: antigravity_tools warning: unused variable: `account_id` 17 |  --> src/modules/tray.rs:70:40 18 |  | 19 | 70 | ... let Ok(Some(account_id)) = modules::get_cur... 20 |  | ^^^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_account_id` 21 | 22 |  Building [=======================> ] 615/617: antigravity_tools warning: variable does not need to be mutable 23 |  --> src/modules/tray.rs:153:24 24 |  | 25 | 153 | ...let Ok(mut account) = modules::load_account... 26 |  | ----^^^^^^^ 27 |  | | 28 |  | help: remove this `mut` 29 |  | 30 |  = note: `#[warn(unused_mut)]` (part of `#[warn(unused)]`) on by default 31 | 32 |  Building [=======================> ] 615/617: antigravity_tools warning: `antigravity_tools` (lib) generated 4 warnings (run `cargo fix --lib -p antigravity_tools` to apply 1 suggestion) 33 |  Building [=======================> ] 616/617: antigravity_tools(bin)  Finished ]8;;https://doc.rust-lang.org/cargo/reference/profiles.html#default-profiles\`dev` profile [unoptimized + debuginfo]]8;;\ target(s) in 0.74s 34 | -------------------------------------------------------------------------------- /src-tauri/output_check_tray.txt: -------------------------------------------------------------------------------- 1 |  Checking antigravity_tools v0.1.0 (/Users/lbjlaq/Desktop/antigravity_tauri/src-tauri) 2 |  Building [=======================> ] 615/617: antigravity_tools error[E0599]: no method named `menu` found for struct `TrayIcon` in the current scope 3 |  --> src/modules/tray.rs:203:39 4 |  | 5 | 203 |  if let Some(menu) = tray.menu() { 6 |  | ^^^^ 7 |  | 8 | help: there is a method `set_menu` with a similar name, but with different arguments 9 |  --> /Users/lbjlaq/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-2.9.5/src/tray/mod.rs:512:3 10 |  | 11 | 512 |  pub fn set_menu(&self, menu: Option) -> crate::Result<()> { 12 |  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 13 | 14 |  Building [=======================> ] 615/617: antigravity_tools warning: unused variable: `name` 15 |  --> src/modules/process.rs:18:13 16 |  | 17 | 18 |  let name = process.name().to_string_los... 18 |  | ^^^^ help: if this is intentional, prefix it with an underscore: `_name` 19 |  | 20 |  = note: `#[warn(unused_variables)]` (part of `#[warn(unused)]`) on by default 21 | 22 |  Building [=======================> ] 615/617: antigravity_tools warning: unused variable: `name` 23 |  --> src/modules/process.rs:188:13 24 |  | 25 | 188 |  let name = process.name().to_string_lo... 26 |  | ^^^^ help: if this is intentional, prefix it with an underscore: `_name` 27 | 28 |  Building [=======================> ] 615/617: antigravity_tools warning: unused variable: `account_id` 29 |  --> src/modules/tray.rs:70:40 30 |  | 31 | 70 | ... let Ok(Some(account_id)) = modules::get_cur... 32 |  | ^^^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_account_id` 33 | 34 |  Building [=======================> ] 615/617: antigravity_tools For more information about this error, try `rustc --explain E0599`. 35 |  Building [=======================> ] 615/617: antigravity_tools warning: `antigravity_tools` (lib) generated 3 warnings 36 | error: could not compile `antigravity_tools` (lib) due to 1 previous error; 3 warnings emitted 37 | -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src-tauri/output_check_final_2.txt: -------------------------------------------------------------------------------- 1 |  Checking antigravity_tools v0.1.0 (/Users/lbjlaq/Desktop/antigravity_tauri/src-tauri) 2 |  Building [=======================> ] 615/617: antigravity_tools error[E0107]: missing generics for trait `IsMenuItem` 3 |  --> src/modules/tray.rs:220:51 4 |  | 5 | 220 | ...tauri::menu::IsMenuItem> = vec![&i_u, &i_q]; 6 |  | ^^^^^^^^^^ expected 1 generic argument 7 |  | 8 | note: trait defined here, with 1 generic parameter: `R` 9 |  --> /Users/lbjlaq/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-2.9.5/src/menu/mod.rs:722:11 10 |  | 11 | 722 | pub trait IsMenuItem: sealed::IsMe... 12 |  | ^^^^^^^^^^ - 13 | help: add missing generic argument 14 |  | 15 | 220 |  let mut items: Vec<&dyn tauri::menu::IsMenuItem> = vec![&i_u, &i_q]; 16 |  | +++ 17 | 18 |  Building [=======================> ] 615/617: antigravity_tools warning: unused variable: `name` 19 |  --> src/modules/process.rs:18:13 20 |  | 21 | 18 |  let name = process.name().to_string_los... 22 |  | ^^^^ help: if this is intentional, prefix it with an underscore: `_name` 23 |  | 24 |  = note: `#[warn(unused_variables)]` (part of `#[warn(unused)]`) on by default 25 | 26 |  Building [=======================> ] 615/617: antigravity_tools warning: unused variable: `name` 27 |  --> src/modules/process.rs:188:13 28 |  | 29 | 188 |  let name = process.name().to_string_lo... 30 |  | ^^^^ help: if this is intentional, prefix it with an underscore: `_name` 31 | 32 |  Building [=======================> ] 615/617: antigravity_tools warning: unused variable: `account_id` 33 |  --> src/modules/tray.rs:70:40 34 |  | 35 | 70 | ... let Ok(Some(account_id)) = modules::get_cur... 36 |  | ^^^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_account_id` 37 | 38 |  Building [=======================> ] 615/617: antigravity_tools For more information about this error, try `rustc --explain E0107`. 39 |  Building [=======================> ] 615/617: antigravity_tools warning: `antigravity_tools` (lib) generated 3 warnings 40 | error: could not compile `antigravity_tools` (lib) due to 1 previous error; 3 warnings emitted 41 | -------------------------------------------------------------------------------- /src/components/accounts/AccountTable.tsx: -------------------------------------------------------------------------------- 1 | import { Account } from '../../types/account'; 2 | import AccountRow from './AccountRow'; 3 | import { useTranslation } from 'react-i18next'; 4 | 5 | interface AccountTableProps { 6 | accounts: Account[]; 7 | selectedIds: Set; 8 | refreshingIds: Set; 9 | onToggleSelect: (id: string) => void; 10 | onToggleAll: () => void; 11 | currentAccountId: string | null; 12 | switchingAccountId: string | null; 13 | onSwitch: (accountId: string) => void; 14 | onRefresh: (accountId: string) => void; 15 | onViewDetails: (accountId: string) => void; 16 | onExport: (accountId: string) => void; 17 | onDelete: (accountId: string) => void; 18 | } 19 | 20 | function AccountTable({ accounts, selectedIds, refreshingIds, onToggleSelect, onToggleAll, currentAccountId, switchingAccountId, onSwitch, onRefresh, onViewDetails, onExport, onDelete }: AccountTableProps) { 21 | const { t } = useTranslation(); 22 | 23 | if (accounts.length === 0) { 24 | return ( 25 |
26 |

{t('accounts.empty.title')}

27 |

{t('accounts.empty.desc')}

28 |
29 | ); 30 | } 31 | 32 | return ( 33 |
34 | 35 | 36 | 37 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | {accounts.map((account) => ( 53 | onToggleSelect(account.id)} 59 | isCurrent={account.id === currentAccountId} 60 | isSwitching={account.id === switchingAccountId} 61 | onSwitch={() => onSwitch(account.id)} 62 | onRefresh={() => onRefresh(account.id)} 63 | onViewDetails={() => onViewDetails(account.id)} 64 | onExport={() => onExport(account.id)} 65 | onDelete={() => onDelete(account.id)} 66 | /> 67 | ))} 68 | 69 |
38 | 0 && selectedIds.size === accounts.length} 42 | onChange={onToggleAll} 43 | /> 44 | {t('accounts.table.email')}{t('accounts.table.quota')}{t('accounts.table.last_used')}{t('accounts.table.actions')}
70 |
71 | ); 72 | } 73 | 74 | export default AccountTable; 75 | -------------------------------------------------------------------------------- /src/components/common/ModalDialog.tsx: -------------------------------------------------------------------------------- 1 | import { AlertTriangle, CheckCircle, XCircle, Info } from 'lucide-react'; 2 | import { createPortal } from 'react-dom'; 3 | 4 | export type ModalType = 'confirm' | 'success' | 'error' | 'info'; 5 | 6 | interface ModalDialogProps { 7 | isOpen: boolean; 8 | title: string; 9 | message: string; 10 | type?: ModalType; 11 | onConfirm: () => void; 12 | onCancel?: () => void; 13 | confirmText?: string; 14 | cancelText?: string; 15 | isDestructive?: boolean; 16 | } 17 | 18 | export default function ModalDialog({ 19 | isOpen, 20 | title, 21 | message, 22 | type = 'confirm', 23 | onConfirm, 24 | onCancel, 25 | confirmText = '确定', 26 | cancelText = '取消', 27 | isDestructive = false 28 | }: ModalDialogProps) { 29 | if (!isOpen) return null; 30 | 31 | const getIcon = () => { 32 | switch (type) { 33 | case 'success': 34 | return ; 35 | case 'error': 36 | return ; 37 | case 'info': 38 | return ; 39 | case 'confirm': 40 | default: 41 | return isDestructive ? : ; 42 | } 43 | }; 44 | 45 | const getIconBg = () => { 46 | switch (type) { 47 | case 'success': return 'bg-green-50 dark:bg-green-900/20'; 48 | case 'error': return 'bg-red-50 dark:bg-red-900/20'; 49 | case 'info': return 'bg-blue-50 dark:bg-blue-900/20'; 50 | case 'confirm': default: return isDestructive ? 'bg-red-50 dark:bg-red-900/20' : 'bg-blue-50 dark:bg-blue-900/20'; 51 | } 52 | }; 53 | 54 | const showCancel = type === 'confirm' && onCancel; 55 | 56 | return createPortal( 57 |
58 | {/* Draggable Top Region */} 59 |
60 | 61 |
62 |
63 |
64 | {getIcon()} 65 |
66 | 67 |

{title}

68 |

{message}

69 | 70 |
71 | {showCancel && ( 72 | 78 | )} 79 | 88 |
89 |
90 |
91 |
92 |
, 93 | document.body 94 | ); 95 | } 96 | -------------------------------------------------------------------------------- /src-tauri/src/modules/oauth_server.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Read, Write}; 2 | use tokio::net::TcpListener; 3 | use tokio::io::{AsyncReadExt, AsyncWriteExt}; 4 | use tokio::sync::oneshot; 5 | use std::sync::{Mutex, OnceLock}; 6 | use tauri::Url; 7 | use crate::modules::oauth; 8 | 9 | // 全局取消 Token 存储 10 | static CANCELLATION_TOKEN: OnceLock>>> = OnceLock::new(); 11 | 12 | /// 获取取消 Token 的 Mutex 13 | fn get_cancellation_token() -> &'static Mutex>> { 14 | CANCELLATION_TOKEN.get_or_init(|| Mutex::new(None)) 15 | } 16 | 17 | /// 取消当前的 OAuth 流程 18 | pub fn cancel_oauth_flow() { 19 | let mutex = get_cancellation_token(); 20 | if let Ok(mut lock) = mutex.lock() { 21 | if let Some(tx) = lock.take() { 22 | let _ = tx.send(()); 23 | crate::modules::logger::log_info("已发送 OAuth 取消信号"); 24 | } 25 | } 26 | } 27 | 28 | /// 启动 OAuth 流程 29 | /// 1. 启动本地服务器监听回调 30 | /// 2. 打开浏览器访问授权页面 31 | /// 3. 等待并捕获 code 32 | /// 4. 交换 token 33 | pub async fn start_oauth_flow(app_handle: tauri::AppHandle) -> Result { 34 | use tauri::Emitter; // 引入 Emitter trait 35 | 36 | // 创建取消通道 37 | let (tx, rx) = oneshot::channel::<()>(); 38 | 39 | // 存储发送端 40 | { 41 | let mutex = get_cancellation_token(); 42 | if let Ok(mut lock) = mutex.lock() { 43 | *lock = Some(tx); 44 | } 45 | } 46 | 47 | // 1. 启动本地监听器 (绑定到随机端口) 48 | // 使用 Tokio TcpListener 实现异步中断 49 | let listener = TcpListener::bind("127.0.0.1:0").await.map_err(|e| format!("无法绑定本地端口: {}", e))?; 50 | let port = listener.local_addr().map_err(|e| format!("无法获取本地端口: {}", e))?.port(); 51 | 52 | // 构造动态 Redirect URI 53 | let redirect_uri = format!("http://localhost:{}/oauth-callback", port); 54 | 55 | // 2. 获取授权 URL 56 | let auth_url = oauth::get_auth_url(&redirect_uri); 57 | 58 | // 发送事件给前端 (用于复制链接功能) 59 | // 忽略发送错误,因为这不影响主流程 60 | let _ = app_handle.emit("oauth-url-generated", &auth_url); 61 | 62 | // 3. 打开浏览器 (使用 tauri_plugin_opener) 63 | use tauri_plugin_opener::OpenerExt; 64 | app_handle.opener().open_url(&auth_url, None::).map_err(|e| format!("无法打开浏览器: {}", e))?; 65 | 66 | // 4. 等待回调 (阻塞接受一个连接),支持取消 67 | let (mut stream, _) = tokio::select! { 68 | res = listener.accept() => { 69 | res.map_err(|e| format!("接受连接失败: {}", e))? 70 | } 71 | _ = rx => { 72 | return Err("用户取消了授权".to_string()); 73 | } 74 | }; 75 | 76 | // 清除取消 token (也可以不做,因为已经被使用了) 77 | { 78 | let mutex = get_cancellation_token(); 79 | if let Ok(mut lock) = mutex.lock() { 80 | *lock = None; 81 | } 82 | } 83 | 84 | let mut buffer = [0; 1024]; 85 | stream.read(&mut buffer).await.map_err(|e| format!("读取请求失败: {}", e))?; 86 | 87 | let request = String::from_utf8_lossy(&buffer); 88 | 89 | // 解析请求行获取 code 90 | // GET /oauth-callback?code=XXXX HTTP/1.1 91 | let code = if let Some(line) = request.lines().next() { 92 | if let Some(path) = line.split_whitespace().nth(1) { 93 | let url = Url::parse(&format!("http://localhost:{}{}", port, path)) 94 | .map_err(|e| format!("URL 解析失败: {}", e))?; 95 | 96 | let pairs = url.query_pairs(); 97 | let mut code = None; 98 | for (key, value) in pairs { 99 | if key == "code" { 100 | code = Some(value.into_owned()); 101 | break; 102 | } 103 | } 104 | code 105 | } else { 106 | None 107 | } 108 | } else { 109 | None 110 | }; 111 | 112 | let response_html = if code.is_some() { 113 | "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\n\r\n 114 | 115 | 116 |

✅ 授权成功!

117 |

您可以关闭此窗口返回应用。

118 | 119 | 120 | " 121 | } else { 122 | "HTTP/1.1 400 Bad Request\r\nContent-Type: text/html\r\n\r\n

❌ 授权失败

" 123 | }; 124 | 125 | stream.write_all(response_html.as_bytes()).await.unwrap_or(()); 126 | stream.flush().await.unwrap_or(()); 127 | 128 | let code = code.ok_or("未能在回调中获取 Authorization Code")?; 129 | 130 | // 5. 交换 Token 131 | oauth::exchange_code(&code, &redirect_uri).await 132 | } 133 | -------------------------------------------------------------------------------- /src/components/accounts/AccountDetailsDialog.tsx: -------------------------------------------------------------------------------- 1 | import { X, Clock, AlertCircle } from 'lucide-react'; 2 | import { createPortal } from 'react-dom'; 3 | import { Account, ModelQuota } from '../../types/account'; 4 | import { formatDate } from '../../utils/format'; 5 | import { useTranslation } from 'react-i18next'; 6 | 7 | interface AccountDetailsDialogProps { 8 | account: Account | null; 9 | onClose: () => void; 10 | } 11 | 12 | export default function AccountDetailsDialog({ account, onClose }: AccountDetailsDialogProps) { 13 | const { t } = useTranslation(); 14 | if (!account) return null; 15 | 16 | return createPortal( 17 |
18 | {/* Draggable Top Region */} 19 |
20 | 21 |
22 | {/* Header */} 23 |
24 |
25 |

{t('accounts.details.title')}

26 |
27 | {account.email} 28 |
29 |
30 | 36 |
37 | 38 | {/* Content */} 39 |
40 | {account.quota?.models?.map((model: ModelQuota) => ( 41 |
42 |
43 | 44 | {model.name} 45 | 46 | = 50 ? 'bg-green-50 text-green-700 dark:bg-green-900/30 dark:text-green-400' : 48 | model.percentage >= 20 ? 'bg-orange-50 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400' : 49 | 'bg-red-50 text-red-700 dark:bg-red-900/30 dark:text-red-400' 50 | }`} 51 | > 52 | {model.percentage}% 53 | 54 |
55 | 56 | {/* Progress Bar */} 57 |
58 |
= 50 ? 'bg-emerald-500' : 60 | model.percentage >= 20 ? 'bg-orange-400' : 61 | 'bg-red-500' 62 | }`} 63 | style={{ width: `${model.percentage}%` }} 64 | >
65 |
66 | 67 |
68 | 69 | {t('accounts.reset_time')}: {formatDate(model.reset_time) || t('common.unknown')} 70 |
71 |
72 | )) || ( 73 |
74 | 75 | {t('accounts.no_data')} 76 |
77 | )} 78 |
79 |
80 |
81 |
, 82 | document.body 83 | ); 84 | } 85 | -------------------------------------------------------------------------------- /scripts/standardize_icon.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | from PIL import Image, ImageDraw 4 | 5 | def create_squircle_mask(size, radius_ratio=0.2): 6 | """ 7 | Creates a squircle (continuous curvature rounded rect) mask. 8 | For simplicity, we uses a standard rounded rectangle which is close enough for most cases, 9 | or we can draw a superellipse. Here we use a high-quality rounded rect. 10 | """ 11 | mask = Image.new('L', size, 0) 12 | draw = ImageDraw.Draw(mask) 13 | w, h = size 14 | # Draw a rounded rectangle 15 | # radius usually ~18-22% for macOS icons 16 | radius = int(min(w, h) * radius_ratio) 17 | draw.rounded_rectangle((0, 0, w, h), radius=radius, fill=255) 18 | return mask 19 | 20 | def process_icon(): 21 | base_dir = os.path.dirname(os.path.abspath(__file__)) 22 | project_root = os.path.dirname(base_dir) 23 | icons_dir = os.path.join(project_root, 'src-tauri', 'icons') 24 | source_icon_path = os.path.join(icons_dir, 'icon.png') 25 | 26 | if not os.path.exists(source_icon_path): 27 | print(f"Error: Source icon not found at {source_icon_path}") 28 | return 29 | 30 | print("Processing icon...") 31 | original = Image.open(source_icon_path).convert('RGBA') 32 | 33 | # macOS standard: Canvas 1024x1024. Actual icon content ~824x824 (approx 80%) to leave room for shadows/padding. 34 | CANVAS_SIZE = 1024 35 | CONTENT_SIZE = 824 36 | 37 | # Resize original to CONTENT_SIZE 38 | # Use LANCZOS for high quality downsampling 39 | resized_content = original.resize((CONTENT_SIZE, CONTENT_SIZE), Image.Resampling.LANCZOS) 40 | 41 | # Create mask 42 | mask = create_squircle_mask((CONTENT_SIZE, CONTENT_SIZE)) 43 | 44 | # Apply mask 45 | resized_content.putalpha(mask) 46 | 47 | # Create final canvas 48 | final_icon = Image.new('RGBA', (CANVAS_SIZE, CANVAS_SIZE), (0, 0, 0, 0)) 49 | 50 | # Paste centered 51 | offset = ((CANVAS_SIZE - CONTENT_SIZE) // 2, (CANVAS_SIZE - CONTENT_SIZE) // 2) 52 | final_icon.paste(resized_content, offset, resized_content) 53 | 54 | # Save master icon 55 | master_icon_path = os.path.join(icons_dir, 'icon_master_squircle.png') 56 | final_icon.save(master_icon_path) 57 | print(f"Saved master icon to {master_icon_path}") 58 | 59 | # Generate .iconset for macOS 60 | iconset_dir = os.path.join(icons_dir, 'icon.iconset') 61 | os.makedirs(iconset_dir, exist_ok=True) 62 | 63 | sizes = [16, 32, 128, 256, 512] 64 | for size in sizes: 65 | # standard size 66 | img = final_icon.resize((size, size), Image.Resampling.LANCZOS) 67 | img.save(os.path.join(iconset_dir, f'icon_{size}x{size}.png')) 68 | 69 | # @2x size 70 | img_2x = final_icon.resize((size * 2, size * 2), Image.Resampling.LANCZOS) 71 | img_2x.save(os.path.join(iconset_dir, f'icon_{size}x{size}@2x.png')) 72 | 73 | # Generate icns using iconutil 74 | print("Generating .icns...") 75 | try: 76 | subprocess.run(['iconutil', '-c', 'icns', iconset_dir, '-o', os.path.join(icons_dir, 'icon.icns')], check=True) 77 | print("Generated icon.icns") 78 | except subprocess.CalledProcessError: 79 | print("Error: Failed to run iconutil. Are you on macOS?") 80 | except FileNotFoundError: 81 | print("Error: iconutil not found.") 82 | 83 | # Generate .ico for Windows (sizes: 16, 32, 48, 64, 128, 256) 84 | print("Generating .ico...") 85 | ico_sizes = [(16, 16), (32, 32), (48, 48), (64, 64), (128, 128), (256, 256)] 86 | final_icon.save(os.path.join(icons_dir, 'icon.ico'), format='ICO', sizes=ico_sizes) 87 | print("Generated icon.ico") 88 | 89 | # Generate standard pngs for Tauri 90 | # Tauri usually looks for 32x32.png, 128x128.png, 128x128@2x.png, icon.png 91 | print("Generating standard PNGs...") 92 | final_icon.save(os.path.join(icons_dir, 'icon.png')) # Overwrite main png 93 | 94 | final_icon.resize((32, 32), Image.Resampling.LANCZOS).save(os.path.join(icons_dir, '32x32.png')) 95 | final_icon.resize((128, 128), Image.Resampling.LANCZOS).save(os.path.join(icons_dir, '128x128.png')) 96 | final_icon.resize((256, 256), Image.Resampling.LANCZOS).save(os.path.join(icons_dir, '128x128@2x.png')) # Tauri often uses this name for 256 97 | 98 | # Generate tray icon 99 | print("Generating tray icon...") 100 | # Standard macOS menu bar icon size is usually 22x22 pts. 101 | # We generate a standard one. 102 | # Note: User requested "unified rounded corners", so we use the squircle version. 103 | # We'll generate 22x22. 104 | tray_size = 22 105 | tray_icon = final_icon.resize((tray_size, tray_size), Image.Resampling.LANCZOS) 106 | tray_icon.save(os.path.join(icons_dir, 'tray-icon.png')) 107 | 108 | # Also generate a large one just in case user wants to swap, but code uses 'tray-icon.png' 109 | # tray_icon_2x = final_icon.resize((44, 44), Image.Resampling.LANCZOS) 110 | # tray_icon_2x.save(os.path.join(icons_dir, 'tray-icon@2x.png')) 111 | 112 | # Cleanup iconset dir 113 | # import shutil 114 | # shutil.rmtree(iconset_dir) 115 | print("Done! Icons updated.") 116 | 117 | if __name__ == '__main__': 118 | process_icon() 119 | -------------------------------------------------------------------------------- /src-tauri/src/utils/protobuf.rs: -------------------------------------------------------------------------------- 1 | /// Protobuf Varint 编码 2 | pub fn encode_varint(mut value: u64) -> Vec { 3 | let mut buf = Vec::new(); 4 | while value >= 0x80 { 5 | buf.push((value & 0x7F | 0x80) as u8); 6 | value >>= 7; 7 | } 8 | buf.push(value as u8); 9 | buf 10 | } 11 | 12 | /// 读取 Protobuf Varint 13 | pub fn read_varint(data: &[u8], offset: usize) -> Result<(u64, usize), String> { 14 | let mut result = 0u64; 15 | let mut shift = 0; 16 | let mut pos = offset; 17 | 18 | loop { 19 | if pos >= data.len() { 20 | return Err("数据不完整".to_string()); 21 | } 22 | let byte = data[pos]; 23 | result |= ((byte & 0x7F) as u64) << shift; 24 | pos += 1; 25 | if byte & 0x80 == 0 { 26 | break; 27 | } 28 | shift += 7; 29 | } 30 | 31 | Ok((result, pos)) 32 | } 33 | 34 | /// 跳过 Protobuf 字段 35 | pub fn skip_field(data: &[u8], offset: usize, wire_type: u8) -> Result { 36 | match wire_type { 37 | 0 => { 38 | // Varint 39 | let (_, new_offset) = read_varint(data, offset)?; 40 | Ok(new_offset) 41 | } 42 | 1 => { 43 | // 64-bit 44 | Ok(offset + 8) 45 | } 46 | 2 => { 47 | // Length-delimited 48 | let (length, content_offset) = read_varint(data, offset)?; 49 | Ok(content_offset + length as usize) 50 | } 51 | 5 => { 52 | // 32-bit 53 | Ok(offset + 4) 54 | } 55 | _ => Err(format!("未知 wire_type: {}", wire_type)), 56 | } 57 | } 58 | 59 | /// 移除指定的 Protobuf 字段 60 | pub fn remove_field(data: &[u8], field_num: u32) -> Result, String> { 61 | let mut result = Vec::new(); 62 | let mut offset = 0; 63 | 64 | while offset < data.len() { 65 | let start_offset = offset; 66 | let (tag, new_offset) = read_varint(data, offset)?; 67 | let wire_type = (tag & 7) as u8; 68 | let current_field = (tag >> 3) as u32; 69 | 70 | if current_field == field_num { 71 | // 跳过此字段 72 | offset = skip_field(data, new_offset, wire_type)?; 73 | } else { 74 | // 保留其他字段 75 | let next_offset = skip_field(data, new_offset, wire_type)?; 76 | result.extend_from_slice(&data[start_offset..next_offset]); 77 | offset = next_offset; 78 | } 79 | } 80 | 81 | Ok(result) 82 | } 83 | 84 | /// 查找指定的 Protobuf 字段内容 (Length-Delimited only) 85 | pub fn find_field(data: &[u8], target_field: u32) -> Result>, String> { 86 | let mut offset = 0; 87 | 88 | while offset < data.len() { 89 | let (tag, new_offset) = match read_varint(data, offset) { 90 | Ok(v) => v, 91 | Err(_) => break, // 数据不完整,停止 92 | }; 93 | 94 | let wire_type = (tag & 7) as u8; 95 | let field_num = (tag >> 3) as u32; 96 | 97 | if field_num == target_field && wire_type == 2 { 98 | let (length, content_offset) = read_varint(data, new_offset)?; 99 | return Ok(Some(data[content_offset..content_offset + length as usize].to_vec())); 100 | } 101 | 102 | // 跳过字段 103 | offset = skip_field(data, new_offset, wire_type)?; 104 | } 105 | 106 | Ok(None) 107 | } 108 | 109 | /// 创建 OAuthTokenInfo (Field 6) 110 | /// 111 | /// 结构: 112 | /// message OAuthTokenInfo { 113 | /// optional string access_token = 1; 114 | /// optional string token_type = 2; 115 | /// optional string refresh_token = 3; 116 | /// optional Timestamp expiry = 4; 117 | /// } 118 | pub fn create_oauth_field(access_token: &str, refresh_token: &str, expiry: i64) -> Vec { 119 | // Field 1: access_token (string, wire_type = 2) 120 | let tag1 = (1 << 3) | 2; 121 | let field1 = { 122 | let mut f = encode_varint(tag1); 123 | f.extend(encode_varint(access_token.len() as u64)); 124 | f.extend(access_token.as_bytes()); 125 | f 126 | }; 127 | 128 | // Field 2: token_type (string, fixed value "Bearer", wire_type = 2) 129 | let tag2 = (2 << 3) | 2; 130 | let token_type = "Bearer"; 131 | let field2 = { 132 | let mut f = encode_varint(tag2); 133 | f.extend(encode_varint(token_type.len() as u64)); 134 | f.extend(token_type.as_bytes()); 135 | f 136 | }; 137 | 138 | // Field 3: refresh_token (string, wire_type = 2) 139 | let tag3 = (3 << 3) | 2; 140 | let field3 = { 141 | let mut f = encode_varint(tag3); 142 | f.extend(encode_varint(refresh_token.len() as u64)); 143 | f.extend(refresh_token.as_bytes()); 144 | f 145 | }; 146 | 147 | // Field 4: expiry (嵌套的 Timestamp 消息, wire_type = 2) 148 | // Timestamp 消息包含: Field 1: seconds (int64, wire_type = 0) 149 | let timestamp_tag = (1 << 3) | 0; // Field 1, varint 150 | let timestamp_msg = { 151 | let mut m = encode_varint(timestamp_tag); 152 | m.extend(encode_varint(expiry as u64)); 153 | m 154 | }; 155 | 156 | let tag4 = (4 << 3) | 2; // Field 4, length-delimited 157 | let field4 = { 158 | let mut f = encode_varint(tag4); 159 | f.extend(encode_varint(timestamp_msg.len() as u64)); 160 | f.extend(timestamp_msg); 161 | f 162 | }; 163 | 164 | // 合并所有字段为 OAuthTokenInfo 消息 165 | let oauth_info = [field1, field2, field3, field4].concat(); 166 | 167 | // 包装为 Field 6 (length-delimited) 168 | let tag6 = (6 << 3) | 2; 169 | let mut field6 = encode_varint(tag6); 170 | field6.extend(encode_varint(oauth_info.len() as u64)); 171 | field6.extend(oauth_info); 172 | 173 | field6 174 | } 175 | -------------------------------------------------------------------------------- /src/stores/useAccountStore.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand'; 2 | import { Account } from '../types/account'; 3 | import * as accountService from '../services/accountService'; 4 | 5 | interface AccountState { 6 | accounts: Account[]; 7 | currentAccount: Account | null; 8 | loading: boolean; 9 | error: string | null; 10 | 11 | // Actions 12 | fetchAccounts: () => Promise; 13 | fetchCurrentAccount: () => Promise; 14 | addAccount: (email: string, refreshToken: string) => Promise; 15 | deleteAccount: (accountId: string) => Promise; 16 | switchAccount: (accountId: string) => Promise; 17 | refreshQuota: (accountId: string) => Promise; 18 | refreshAllQuotas: () => Promise; 19 | 20 | // 新增 actions 21 | startOAuthLogin: () => Promise; 22 | cancelOAuthLogin: () => Promise; 23 | importV1Accounts: () => Promise; 24 | importFromDb: () => Promise; 25 | } 26 | 27 | export const useAccountStore = create((set, get) => ({ 28 | accounts: [], 29 | currentAccount: null, 30 | loading: false, 31 | error: null, 32 | 33 | fetchAccounts: async () => { 34 | set({ loading: true, error: null }); 35 | try { 36 | console.log('[Store] Fetching accounts...'); 37 | const accounts = await accountService.listAccounts(); 38 | set({ accounts, loading: false }); 39 | } catch (error) { 40 | console.error('[Store] Fetch accounts failed:', error); 41 | set({ error: String(error), loading: false }); 42 | } 43 | }, 44 | 45 | fetchCurrentAccount: async () => { 46 | set({ loading: true, error: null }); 47 | try { 48 | const account = await accountService.getCurrentAccount(); 49 | set({ currentAccount: account, loading: false }); 50 | } catch (error) { 51 | set({ error: String(error), loading: false }); 52 | } 53 | }, 54 | 55 | addAccount: async (email: string, refreshToken: string) => { 56 | set({ loading: true, error: null }); 57 | try { 58 | await accountService.addAccount(email, refreshToken); 59 | await get().fetchAccounts(); 60 | set({ loading: false }); 61 | } catch (error) { 62 | set({ error: String(error), loading: false }); 63 | throw error; 64 | } 65 | }, 66 | 67 | deleteAccount: async (accountId: string) => { 68 | set({ loading: true, error: null }); 69 | try { 70 | await accountService.deleteAccount(accountId); 71 | await get().fetchAccounts(); 72 | set({ loading: false }); 73 | } catch (error) { 74 | set({ error: String(error), loading: false }); 75 | throw error; 76 | } 77 | }, 78 | 79 | switchAccount: async (accountId: string) => { 80 | set({ loading: true, error: null }); 81 | try { 82 | await accountService.switchAccount(accountId); 83 | await get().fetchCurrentAccount(); 84 | set({ loading: false }); 85 | } catch (error) { 86 | set({ error: String(error), loading: false }); 87 | throw error; 88 | } 89 | }, 90 | 91 | refreshQuota: async (accountId: string) => { 92 | set({ loading: true, error: null }); 93 | try { 94 | await accountService.fetchAccountQuota(accountId); 95 | await get().fetchAccounts(); 96 | set({ loading: false }); 97 | } catch (error) { 98 | set({ error: String(error), loading: false }); 99 | throw error; 100 | } 101 | }, 102 | 103 | refreshAllQuotas: async () => { 104 | set({ loading: true, error: null }); 105 | try { 106 | const stats = await accountService.refreshAllQuotas(); 107 | await get().fetchAccounts(); 108 | set({ loading: false }); 109 | return stats; 110 | } catch (error) { 111 | set({ error: String(error), loading: false }); 112 | throw error; 113 | } 114 | }, 115 | 116 | startOAuthLogin: async () => { 117 | set({ loading: true, error: null }); 118 | try { 119 | await accountService.startOAuthLogin(); 120 | await get().fetchAccounts(); 121 | set({ loading: false }); 122 | } catch (error) { 123 | set({ error: String(error), loading: false }); 124 | throw error; 125 | } 126 | }, 127 | 128 | cancelOAuthLogin: async () => { 129 | try { 130 | await accountService.cancelOAuthLogin(); 131 | set({ loading: false, error: null }); 132 | } catch (error) { 133 | console.error('[Store] Cancel OAuth failed:', error); 134 | } 135 | }, 136 | 137 | importV1Accounts: async () => { 138 | set({ loading: true, error: null }); 139 | try { 140 | await accountService.importV1Accounts(); 141 | await get().fetchAccounts(); 142 | set({ loading: false }); 143 | } catch (error) { 144 | set({ error: String(error), loading: false }); 145 | throw error; 146 | } 147 | }, 148 | 149 | importFromDb: async () => { 150 | set({ loading: true, error: null }); 151 | try { 152 | await accountService.importFromDb(); 153 | await get().fetchAccounts(); 154 | set({ loading: false }); 155 | } catch (error) { 156 | set({ error: String(error), loading: false }); 157 | throw error; 158 | } 159 | }, 160 | })); 161 | -------------------------------------------------------------------------------- /src/components/dashboard/BestAccounts.tsx: -------------------------------------------------------------------------------- 1 | import { TrendingUp } from 'lucide-react'; 2 | import { Account } from '../../types/account'; 3 | 4 | interface BestAccountsProps { 5 | accounts: Account[]; 6 | currentAccountId?: string; 7 | onSwitch?: (accountId: string) => void; 8 | } 9 | 10 | import { useTranslation } from 'react-i18next'; 11 | 12 | function BestAccounts({ accounts, currentAccountId, onSwitch }: BestAccountsProps) { 13 | const { t } = useTranslation(); 14 | // 1. 获取按配额排序的列表 (排除当前账号) 15 | const geminiSorted = accounts 16 | .filter(a => a.id !== currentAccountId) 17 | .map(a => ({ 18 | ...a, 19 | quotaVal: a.quota?.models.find(m => m.name.toLowerCase().includes('gemini'))?.percentage || 0, 20 | })) 21 | .filter(a => a.quotaVal > 0) 22 | .sort((a, b) => b.quotaVal - a.quotaVal); 23 | 24 | const claudeSorted = accounts 25 | .filter(a => a.id !== currentAccountId) 26 | .map(a => ({ 27 | ...a, 28 | quotaVal: a.quota?.models.find(m => m.name.toLowerCase().includes('claude'))?.percentage || 0, 29 | })) 30 | .filter(a => a.quotaVal > 0) 31 | .sort((a, b) => b.quotaVal - a.quotaVal); 32 | 33 | let bestGemini = geminiSorted[0]; 34 | let bestClaude = claudeSorted[0]; 35 | 36 | // 2. 如果推荐是同一个账号,且有其他选择,尝试寻找最优的"不同账号"组合 37 | if (bestGemini && bestClaude && bestGemini.id === bestClaude.id) { 38 | const nextGemini = geminiSorted[1]; 39 | const nextClaude = claudeSorted[1]; 40 | 41 | // 方案A: 保持 Gemini 最优,换 Claude 次优 42 | // 方案B: 换 Gemini 次优,保持 Claude 最优 43 | // 比较标准:两者配额之和最大化 (或者优先保住 100% 的那个) 44 | 45 | const scoreA = bestGemini.quotaVal + (nextClaude?.quotaVal || 0); 46 | const scoreB = (nextGemini?.quotaVal || 0) + bestClaude.quotaVal; 47 | 48 | if (nextClaude && (!nextGemini || scoreA >= scoreB)) { 49 | // 选方案A:换 Claude 50 | bestClaude = nextClaude; 51 | } else if (nextGemini) { 52 | // 选方案B:换 Gemini 53 | bestGemini = nextGemini; 54 | } 55 | // 如果都没有次优解(例如只有一个账号),则保持原样 56 | } 57 | 58 | // 构造最终用于显示的视图模型 (兼容原有渲染逻辑) 59 | const bestGeminiRender = bestGemini ? { ...bestGemini, geminiQuota: bestGemini.quotaVal } : undefined; 60 | const bestClaudeRender = bestClaude ? { ...bestClaude, claudeQuota: bestClaude.quotaVal } : undefined; 61 | 62 | return ( 63 |
64 |

65 | 66 | {t('dashboard.best_accounts')} 67 |

68 | 69 |
70 | {/* Gemini 最佳 */} 71 | {bestGeminiRender && ( 72 |
73 |
74 |
{t('dashboard.for_gemini')}
75 |
76 | {bestGeminiRender.email} 77 |
78 |
79 |
80 | {bestGeminiRender.geminiQuota}% 81 |
82 |
83 | )} 84 | 85 | {/* Claude 最佳 */} 86 | {bestClaudeRender && ( 87 |
88 |
89 |
{t('dashboard.for_claude')}
90 |
91 | {bestClaudeRender.email} 92 |
93 |
94 |
95 | {bestClaudeRender.claudeQuota}% 96 |
97 |
98 | )} 99 | 100 | {(!bestGeminiRender && !bestClaudeRender) && ( 101 |
102 | {t('accounts.no_data')} 103 |
104 | )} 105 |
106 | 107 | {(bestGeminiRender || bestClaudeRender) && onSwitch && ( 108 |
109 | 125 |
126 | )} 127 |
128 | ); 129 | 130 | } 131 | 132 | export default BestAccounts; 133 | -------------------------------------------------------------------------------- /test_tauri_commands.js: -------------------------------------------------------------------------------- 1 | // Tauri 命令集成测试脚本 2 | // 在浏览器控制台中执行此脚本 3 | 4 | console.log('🧪 开始 Tauri 命令集成测试...\n'); 5 | 6 | const { invoke } = window.__TAURI__.core; 7 | const results = { 8 | passed: 0, 9 | failed: 0, 10 | tests: [] 11 | }; 12 | 13 | // 辅助函数 14 | function logTest(name, status, data, error = null) { 15 | const emoji = status === 'PASS' ? '✅' : '❌'; 16 | console.log(`${emoji} ${name}: ${status}`); 17 | if (data) console.log(' 数据:', data); 18 | if (error) console.log(' 错误:', error); 19 | 20 | results.tests.push({ name, status, data, error }); 21 | if (status === 'PASS') results.passed++; 22 | else results.failed++; 23 | } 24 | 25 | // 测试 1: 加载配置 26 | async function test1_loadConfig() { 27 | console.log('\n📝 测试 1: 加载配置'); 28 | try { 29 | const config = await invoke('load_config'); 30 | logTest('load_config', 'PASS', config); 31 | return config; 32 | } catch (error) { 33 | logTest('load_config', 'FAIL', null, error); 34 | return null; 35 | } 36 | } 37 | 38 | // 测试 2: 列出账号 39 | async function test2_listAccounts() { 40 | console.log('\n📝 测试 2: 列出所有账号'); 41 | try { 42 | const accounts = await invoke('list_accounts'); 43 | logTest('list_accounts', 'PASS', `找到 ${accounts.length} 个账号`); 44 | return accounts; 45 | } catch (error) { 46 | logTest('list_accounts', 'FAIL', null, error); 47 | return []; 48 | } 49 | } 50 | 51 | // 测试 3: 获取当前账号 52 | async function test3_getCurrentAccount() { 53 | console.log('\n📝 测试 3: 获取当前账号'); 54 | try { 55 | const current = await invoke('get_current_account'); 56 | logTest('get_current_account', 'PASS', current); 57 | return current; 58 | } catch (error) { 59 | logTest('get_current_account', 'FAIL', null, error); 60 | return null; 61 | } 62 | } 63 | 64 | // 测试 4: 添加测试账号 65 | async function test4_addAccount() { 66 | console.log('\n📝 测试 4: 添加测试账号'); 67 | try { 68 | const testToken = { 69 | access_token: 'test_access_token_' + Date.now(), 70 | refresh_token: 'test_refresh_token_' + Date.now(), 71 | expires_at: new Date(Date.now() + 3600000).toISOString() 72 | }; 73 | 74 | const newAccount = await invoke('add_account', { 75 | email: 'test_' + Date.now() + '@example.com', 76 | token: testToken 77 | }); 78 | 79 | logTest('add_account', 'PASS', { 80 | id: newAccount.id, 81 | email: newAccount.email 82 | }); 83 | return newAccount; 84 | } catch (error) { 85 | logTest('add_account', 'FAIL', null, error); 86 | return null; 87 | } 88 | } 89 | 90 | // 测试 5: 切换账号 91 | async function test5_switchAccount(accountId) { 92 | console.log('\n📝 测试 5: 切换账号'); 93 | if (!accountId) { 94 | logTest('switch_account', 'SKIP', '没有可切换的账号'); 95 | return; 96 | } 97 | 98 | try { 99 | await invoke('switch_account', { accountId }); 100 | logTest('switch_account', 'PASS', `切换到 ${accountId}`); 101 | 102 | // 验证切换成功 103 | const current = await invoke('get_current_account'); 104 | if (current && current.id === accountId) { 105 | logTest('switch_account_verify', 'PASS', '切换验证成功'); 106 | } else { 107 | logTest('switch_account_verify', 'FAIL', '切换验证失败'); 108 | } 109 | } catch (error) { 110 | logTest('switch_account', 'FAIL', null, error); 111 | } 112 | } 113 | 114 | // 测试 6: 保存配置 115 | async function test6_saveConfig() { 116 | console.log('\n📝 测试 6: 保存配置'); 117 | try { 118 | const newConfig = { 119 | language: 'zh-CN', 120 | theme: 'dark', 121 | auto_refresh: true, 122 | refresh_interval: 30, 123 | auto_sync: false, 124 | sync_interval: 10 125 | }; 126 | 127 | await invoke('save_config', { config: newConfig }); 128 | logTest('save_config', 'PASS', newConfig); 129 | 130 | // 验证保存成功 131 | const loaded = await invoke('load_config'); 132 | if (JSON.stringify(loaded) === JSON.stringify(newConfig)) { 133 | logTest('save_config_verify', 'PASS', '配置验证成功'); 134 | } else { 135 | logTest('save_config_verify', 'FAIL', '配置验证失败'); 136 | } 137 | } catch (error) { 138 | logTest('save_config', 'FAIL', null, error); 139 | } 140 | } 141 | 142 | // 测试 7: 删除账号 143 | async function test7_deleteAccount(accountId) { 144 | console.log('\n📝 测试 7: 删除测试账号'); 145 | if (!accountId) { 146 | logTest('delete_account', 'SKIP', '没有可删除的账号'); 147 | return; 148 | } 149 | 150 | try { 151 | await invoke('delete_account', { accountId }); 152 | logTest('delete_account', 'PASS', `删除账号 ${accountId}`); 153 | 154 | // 验证删除成功 155 | const accounts = await invoke('list_accounts'); 156 | if (!accounts.some(a => a.id === accountId)) { 157 | logTest('delete_account_verify', 'PASS', '删除验证成功'); 158 | } else { 159 | logTest('delete_account_verify', 'FAIL', '删除验证失败'); 160 | } 161 | } catch (error) { 162 | logTest('delete_account', 'FAIL', null, error); 163 | } 164 | } 165 | 166 | // 主测试流程 167 | async function runAllTests() { 168 | console.log('='.repeat(60)); 169 | console.log('🚀 Tauri 命令集成测试'); 170 | console.log('='.repeat(60)); 171 | 172 | // 执行测试 173 | await test1_loadConfig(); 174 | const initialAccounts = await test2_listAccounts(); 175 | await test3_getCurrentAccount(); 176 | const newAccount = await test4_addAccount(); 177 | 178 | if (newAccount) { 179 | await test5_switchAccount(newAccount.id); 180 | } 181 | 182 | await test6_saveConfig(); 183 | 184 | if (newAccount) { 185 | await test7_deleteAccount(newAccount.id); 186 | } 187 | 188 | // 输出总结 189 | console.log('\n' + '='.repeat(60)); 190 | console.log('📊 测试总结'); 191 | console.log('='.repeat(60)); 192 | console.log(`✅ 通过: ${results.passed}`); 193 | console.log(`❌ 失败: ${results.failed}`); 194 | console.log(`📝 总计: ${results.tests.length}`); 195 | console.log(`🎯 成功率: ${((results.passed / results.tests.length) * 100).toFixed(1)}%`); 196 | 197 | // 返回结果 198 | return results; 199 | } 200 | 201 | // 执行测试 202 | runAllTests().then(results => { 203 | console.log('\n✨ 测试完成! 结果已保存到 window.testResults'); 204 | window.testResults = results; 205 | }); 206 | -------------------------------------------------------------------------------- /src-tauri/src/modules/quota.rs: -------------------------------------------------------------------------------- 1 | use reqwest; 2 | use serde::{Deserialize, Serialize}; 3 | use serde_json::json; 4 | use crate::models::QuotaData; 5 | 6 | const QUOTA_API_URL: &str = "https://cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels"; 7 | const LOAD_PROJECT_API_URL: &str = "https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist"; 8 | const USER_AGENT: &str = "antigravity/1.11.3 Darwin/arm64"; 9 | 10 | #[derive(Debug, Serialize, Deserialize)] 11 | struct QuotaResponse { 12 | models: std::collections::HashMap, 13 | } 14 | 15 | #[derive(Debug, Serialize, Deserialize)] 16 | struct ModelInfo { 17 | #[serde(rename = "quotaInfo")] 18 | quota_info: Option, 19 | } 20 | 21 | #[derive(Debug, Serialize, Deserialize)] 22 | struct QuotaInfo { 23 | #[serde(rename = "remainingFraction")] 24 | remaining_fraction: Option, 25 | #[serde(rename = "resetTime")] 26 | reset_time: Option, 27 | } 28 | 29 | #[derive(Debug, Deserialize)] 30 | struct LoadProjectResponse { 31 | #[serde(rename = "cloudaicompanionProject")] 32 | project_id: Option, 33 | } 34 | 35 | /// 创建配置好的 HTTP Client 36 | fn create_client() -> reqwest::Client { 37 | reqwest::Client::builder() 38 | .timeout(std::time::Duration::from_secs(15)) 39 | .build() 40 | .unwrap_or_default() 41 | } 42 | 43 | /// 获取 Project ID 44 | async fn fetch_project_id(access_token: &str) -> Option { 45 | let client = create_client(); 46 | let body = json!({ 47 | "metadata": { 48 | "ideType": "ANTIGRAVITY" 49 | } 50 | }); 51 | 52 | // 简单的重试 53 | for _ in 0..2 { 54 | match client 55 | .post(LOAD_PROJECT_API_URL) 56 | .bearer_auth(access_token) 57 | .header("User-Agent", USER_AGENT) 58 | .json(&body) 59 | .send() 60 | .await 61 | { 62 | Ok(res) => { 63 | if res.status().is_success() { 64 | if let Ok(data) = res.json::().await { 65 | return data.project_id; 66 | } 67 | } 68 | } 69 | Err(_) => { 70 | tokio::time::sleep(std::time::Duration::from_millis(500)).await; 71 | } 72 | } 73 | } 74 | None 75 | } 76 | 77 | /// 查询账号配额 78 | pub async fn fetch_quota(access_token: &str) -> crate::error::AppResult { 79 | use crate::error::AppError; 80 | crate::modules::logger::log_info("开始外部查询配额..."); 81 | let client = create_client(); 82 | 83 | // 1. 获取 Project ID 84 | let project_id = fetch_project_id(access_token).await; 85 | crate::modules::logger::log_info(&format!("Project ID 获取结果: {:?}", project_id)); 86 | 87 | // 2. 构建请求体 88 | let mut payload = serde_json::Map::new(); 89 | if let Some(pid) = project_id { 90 | payload.insert("project".to_string(), json!(pid)); 91 | } 92 | 93 | let url = QUOTA_API_URL; 94 | let max_retries = 3; 95 | let mut last_error: Option = None; 96 | 97 | crate::modules::logger::log_info(&format!("发送配额请求至 {}", url)); 98 | 99 | for attempt in 1..=max_retries { 100 | match client 101 | .post(url) 102 | .bearer_auth(access_token) 103 | .header("User-Agent", USER_AGENT) 104 | .json(&json!(payload)) 105 | .send() 106 | .await 107 | { 108 | Ok(response) => { 109 | // 将 HTTP 错误状态转换为 AppError 110 | if let Err(_) = response.error_for_status_ref() { 111 | let status = response.status(); 112 | // 策略: 先获取需要的 status/url 信息用于日志,消耗 response print body, 113 | // 然后我们其实无法再使用 response 生成 error。 114 | // 幸好我们在 if let 里面已经拿到了 err (owned)。 115 | 116 | if attempt < max_retries { 117 | let text = response.text().await.unwrap_or_default(); 118 | crate::modules::logger::log_warn(&format!("API 错误: {} - {} (尝试 {}/{})", status, text, attempt, max_retries)); 119 | last_error = Some(AppError::Unknown(format!("HTTP {} - {}", status, text))); 120 | tokio::time::sleep(std::time::Duration::from_secs(1)).await; 121 | continue; 122 | } else { 123 | let text = response.text().await.unwrap_or_default(); 124 | return Err(AppError::Unknown(format!("API 错误: {} - {}", status, text))); 125 | } 126 | } 127 | 128 | let quota_response: QuotaResponse = response 129 | .json() 130 | .await 131 | .map_err(|e| AppError::Network(e))?; 132 | 133 | let mut quota_data = QuotaData::new(); 134 | 135 | for (name, info) in quota_response.models { 136 | if let Some(quota_info) = info.quota_info { 137 | let percentage = quota_info.remaining_fraction 138 | .map(|f| (f * 100.0) as i32) 139 | .unwrap_or(0); 140 | 141 | let reset_time = quota_info.reset_time.unwrap_or_default(); 142 | 143 | // 只保存我们关心的模型 144 | if name.contains("gemini") || name.contains("claude") { 145 | quota_data.add_model(name, percentage, reset_time); 146 | } 147 | } 148 | } 149 | 150 | return Ok(quota_data); 151 | }, 152 | Err(e) => { 153 | crate::modules::logger::log_warn(&format!("请求失败: {} (尝试 {}/{})", e, attempt, max_retries)); 154 | last_error = Some(AppError::Network(e)); 155 | if attempt < max_retries { 156 | tokio::time::sleep(std::time::Duration::from_secs(1)).await; 157 | } 158 | } 159 | } 160 | } 161 | 162 | Err(last_error.unwrap_or_else(|| AppError::Unknown("配额查询失败".to_string()))) 163 | } 164 | 165 | /// 批量查询所有账号配额 (备用功能) 166 | #[allow(dead_code)] 167 | pub async fn fetch_all_quotas(accounts: Vec<(String, String)>) -> Vec<(String, crate::error::AppResult)> { 168 | let mut results = Vec::new(); 169 | 170 | for (account_id, access_token) in accounts { 171 | let result = fetch_quota(&access_token).await; 172 | results.push((account_id, result)); 173 | } 174 | 175 | results 176 | } 177 | -------------------------------------------------------------------------------- /src/components/layout/Navbar.tsx: -------------------------------------------------------------------------------- 1 | import { Link, useLocation } from 'react-router-dom'; 2 | import { Sun, Moon } from 'lucide-react'; 3 | import { useTranslation } from 'react-i18next'; 4 | import { useConfigStore } from '../../stores/useConfigStore'; 5 | 6 | function Navbar() { 7 | const location = useLocation(); 8 | const { t, i18n } = useTranslation(); 9 | const { config, saveConfig } = useConfigStore(); 10 | 11 | const navItems = [ 12 | { path: '/', label: t('nav.dashboard') }, 13 | { path: '/accounts', label: t('nav.accounts') }, 14 | { path: '/settings', label: t('nav.settings') }, 15 | ]; 16 | 17 | const isActive = (path: string) => { 18 | if (path === '/') { 19 | return location.pathname === '/'; 20 | } 21 | return location.pathname.startsWith(path); 22 | }; 23 | 24 | const toggleTheme = async (event: React.MouseEvent) => { 25 | if (!config) return; 26 | 27 | const newTheme = config.theme === 'light' ? 'dark' : 'light'; 28 | 29 | // 如果浏览器支持 View Transition API 30 | if ('startViewTransition' in document) { 31 | const x = event.clientX; 32 | const y = event.clientY; 33 | const endRadius = Math.hypot( 34 | Math.max(x, window.innerWidth - x), 35 | Math.max(y, window.innerHeight - y) 36 | ); 37 | 38 | // @ts-ignore 39 | const transition = document.startViewTransition(async () => { 40 | await saveConfig({ 41 | ...config, 42 | theme: newTheme, 43 | language: config.language 44 | }); 45 | }); 46 | 47 | transition.ready.then(() => { 48 | const clipPath = [ 49 | `circle(0px at ${x}px ${y}px)`, 50 | `circle(${endRadius}px at ${x}px ${y}px)` 51 | ]; 52 | 53 | document.documentElement.animate( 54 | { 55 | clipPath: clipPath 56 | }, 57 | { 58 | duration: 500, 59 | easing: 'ease-in-out', 60 | pseudoElement: '::view-transition-new(root)' 61 | } 62 | ); 63 | }); 64 | } else { 65 | // 降级方案:直接切换 66 | await saveConfig({ 67 | ...config, 68 | theme: newTheme, 69 | language: config.language 70 | }); 71 | } 72 | }; 73 | 74 | const toggleLanguage = async () => { 75 | if (!config) return; 76 | const newLang = config.language === 'zh' ? 'en' : 'zh'; 77 | await saveConfig({ 78 | ...config, 79 | language: newLang, 80 | theme: config.theme 81 | }); 82 | i18n.changeLanguage(newLang); 83 | }; 84 | 85 | return ( 86 | 151 | ); 152 | } 153 | 154 | export default Navbar; 155 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Antigravity Tools 🚀 2 | 3 |
4 | Antigravity Logo 5 | 6 |

Professional Account Management for AI Services

7 |

Manage your Gemini / Claude accounts with ease. Unlimited Possibilities.

8 | 9 |

10 | 11 | Version 12 | 13 | Tauri 14 | React 15 | License 16 |

17 | 18 |

19 | 📥 下载最新版 (macOS/Windows/Linux) • 20 | ✨ 核心特性 • 21 | 🆚 版本对比 22 |

23 | 24 |

25 | 🇨🇳 简体中文 | 26 | 🇺🇸 English 27 |

28 |
29 | 30 | --- 31 | 32 | --- 33 | 34 |
35 | Antigravity Dark Mode 36 |

(Deep Dark: 沉浸式暗色模式,专注开发)

37 |
38 | 39 | ## 🎨 界面预览 (Gallery) 40 | 41 |
42 | 43 | | **Light Mode (清爽明亮)** | **Dark Mode (深邃护眼)** | 44 | | :---: | :---: | 45 | | | | 46 | | **仪表盘 Dashboard** | **账号管理 Accounts** | 47 | 48 | | | | 49 | | **列表视图 List View** | **全局设置 Settings** | 50 | 51 |
52 | 53 | --- 54 | 55 | **Antigravity Tools** 是一款专为 AI 开发者和重度用户打造的 **现代化账号管理工具**。 56 | 57 | 作为 [Antigravity Manager](https://github.com/lbjlaq/Antigravity-Manager) 的 2.0 重构版本,它采用了高性能的 **[Tauri v2](https://v2.tauri.app/)** + **[React](https://react.dev/)** 技术栈,将原本笨重的 Python GUI 进化为轻量、极速的原生应用。 58 | 59 | 它可以帮助你轻松管理数十个 **Google Gemini**、**Claude 3.5** 等 AI 服务账号,实时监控配额(Quota),并在配额耗尽时智能切换,助你实现 "无限" 的 AI 调用体验。 60 | 61 | > ⚠️ **注意**: 本项目仓库地址保持不变,继续沿用 [lbjlaq/Antigravity-Manager](https://github.com/lbjlaq/Antigravity-Manager)。 62 | > 63 | > **寻找 1.0 版本?** 64 | > v1.0 (Python/Flet) 版本的完整源码已归档至 [v1 分支](https://github.com/lbjlaq/Antigravity-Manager/tree/v1)。如需查看或维护旧版,请切换分支查看。 65 | 66 | ## 🆚 为什么选择 2.0 ? (Comparison) 67 | 68 | | 特性 Comparison | 🐢 v1.0 (Legacy) | 🚀 v2.0 (New) | 提升 | 69 | | :--- | :--- | :--- | :--- | 70 | | **技术核心** | Python + Flet | **Rust (Tauri)** + **React** | **性能质变** | 71 | | **安装包大小** | ~80 MB | **~10 MB** | **体积减少 87%** | 72 | | **启动速度** | 慢 (需加载 Python 解释器) | **秒开** (原生二进制) | **极速响应** | 73 | | **内存占用** | 高 (>200MB) | **极低** (<50MB) | **更省资源** | 74 | | **界面交互** | 基础 Material 风格 | **现代化 Glassmorphism** | **颜值正义** | 75 | | **安全性** | 明文/简单混淆 | **本地 JSON 存储** | **透明可控** | 76 | | **扩展性** | 难 (Python 依赖地狱) | **易** (标准 Web 技术栈) | **生态丰富** | 77 | 78 | ## ✨ 核心特性 (Features) 79 | 80 | ### 📊 仪表盘 (Dashboard) 81 | - **全局概览**: 实时展示账号总数、各模型平均配额,健康度一目了然。 82 | - **智能推荐**: 自动筛选当前配额最充足的 "最佳账号",支持一键快速切换,始终使用最优资源。 83 | - **状态监控**: 实时高亮显示低配额告警账号,避免开发中断。 84 | 85 | ### 👥 账号管理 (Account Management) 86 | - **多渠道导入**: 87 | - 🔥 **OAuth 授权**: 支持拉起浏览器进行 Google 登录授权,自动获取 Token (推荐)。 88 | - 📋 **手动添加**: 支持直接粘贴 Refresh Token 进行添加。 89 | - 📂 **V1 迁移**: 支持从 v1 版本 (`~/.antigravity-agent`) 自动扫描并批量导入旧数据。 90 | - 🔄 **本地同步**: 支持从 IDE (Cursor/Windsurf) 本地数据库自动读取并导入当前登录账号。 91 | - **批量操作**: 提供批量刷新配额、批量导出备份 (JSON)、批量删除功能。 92 | - **搜索过滤**: 支持按邮箱关键字快速检索,管理数十个账号依然轻松。 93 | 94 | ### 🔄 配额同步 (Quota Sync) 95 | - **自动刷新**: 可配置后台自动定时轮询所有账号的最新配额信息。 96 | - **Token 保活**: 内置 Token 自动刷新机制,过期自动续期,确保连接时刻有效。 97 | - **精准展示**: 清晰展示 Gemini / Claude 等不同模型的具体剩余百分比和重置时间。 98 | 99 | ### 🛠️ 系统集成 (System Integration) 100 | - **托盘常驻**: 程序可最小化至系统托盘,不占用任务栏空间,后台静默运行。 101 | - **快捷操作**: 托盘菜单支持一键查看当前账号配额、快速切换下一个可用账号。 102 | - **安全存储**: 采用本地 JSON 格式存储,所有 Token 数据仅保存在用户设备,绝不上传云端。 103 | 104 | ### ⚙️ 个性化设置 (Settings) 105 | - **国际化**: 原生支持 **简体中文** / **English** 实时切换。 106 | - **主题适配**: 完美适配系统的深色 (Dark Mode) / 浅色模式,夜间使用更护眼。 107 | - **数据管理**: 支持自定义数据导出路径,并提供日志缓存一键清理功能。 108 | 109 | ## 🛠️ 技术栈 110 | 111 | 本项目采用前沿的现代技术栈构建,确保了应用的高性能与可维护性: 112 | 113 | | 模块 | 技术选型 | 说明 | 114 | | :--- | :--- | :--- | 115 | | **Frontend** | React 18 + TypeScript | UI 构建与逻辑处理 | 116 | | **UI Framework** | TailwindCSS + DaisyUI | 现代化原子类样式库 | 117 | | **Backend** | Tauri v2 (Rust) | 高性能、安全的系统底层交互 | 118 | | **Storage** | Local JSON | 本地配置与数据存储 | 119 | | **State** | Zustand | 轻量级全局状态管理 | 120 | | **Network** | Reqwest (Async) | 异步网络请求处理 | 121 | 122 | ## 📦 安装与运行 123 | 124 | ### 📥 下载安装 125 | 126 | 前往 [Releases 页面](https://github.com/lbjlaq/Antigravity-Manager/releases) 下载对应系统的安装包: 127 | 128 | - **macOS**: 支持 Intel (`.dmg`) 和 Apple Silicon (`.dmg`) 129 | - **Windows**: `.exe` 安装包 130 | - **Linux**: `.deb` 或 `.AppImage` *(理论支持,尚未经完整测试,欢迎反馈)* 131 | 132 | ### 💻 开发环境启动 133 | 134 | 如果您是开发者,想要贡献代码: 135 | 136 | ```bash 137 | # 1. 克隆项目 138 | git clone https://github.com/lbjlaq/antigravity-tools.git 139 | 140 | # 2. 安装前端依赖 141 | npm install 142 | 143 | # 3. 启动开发模式 (Frontend + Backend) 144 | npm run tauri dev 145 | ``` 146 | 147 | ### 🏗️ 构建发布 148 | 149 | ```bash 150 | # 构建通用 macOS 应用 (同时支持 Intel & Apple Silicon) 151 | npm run build:universal 152 | ``` 153 | 154 | ## ❓ 常见问题 (FAQ) 155 | 156 | ### ⚠️ 打开应用提示 "已损坏" 或 "无法打开"? 157 | 158 | 如果在 macOS 上打开应用时提示 **“Antigravity Tools 已损坏,无法打开”**,这是 macOS Gatekeeper 对未签名应用的默认拦截机制。 159 | 160 | **解决方法:** 161 | 162 | 1. 打开终端 (Terminal)。 163 | 2. 复制并执行以下命令 (可能需要输入密码): 164 | 165 | ```bash 166 | sudo xattr -rd com.apple.quarantine "/Applications/Antigravity Tools.app" 167 | ``` 168 | 169 | > 注意:请根据实际安装位置调整路径,如果安装在“应用程序”目录,通常就是上面的路径。 170 | 171 | ## 📅 更新日志 (Changelog) 172 | 173 | ### v2.1.2 (2025-12-16) 174 | - **🪟 Windows 兼容性**: 修复了在 Windows 下无法打开数据目录的问题。 175 | - **📁 路径显示**: 设置页面现在显示数据目录的完整绝对路径,方便查找。 176 | 177 | ### v2.1.0 (2025-12-15) 178 | - **🔥 OAuth 重构**: 179 | - 修复端口冲突问题 (改为随机端口)。 180 | - 新增 **"复制链接"** 功能,支持手动在浏览器完成验证。 181 | - 新增 **"取消授权"** 按钮,支持主动释放资源。 182 | - **🎨 图标升级**: 183 | - 全新设计的 macOS 风格圆角图标 (Squircle)。 184 | - 优化托盘图标显示效果。 185 | - 修复旧版 macOS 下图标显示过大的问题。 186 | - **📖 文档**: 新增常见问题 (FAQ) 指引。 187 | 188 | ## 👤 作者 189 | 190 | **Ctrler** 191 | 192 | - 💬 微信公众号: `Ctrler` 193 | - 🐙 GitHub: [@lbjlaq](https://github.com/lbjlaq) 194 | 195 | ## 📄 版权说明 196 | 197 | Copyright © 2025 Antigravity. All rights reserved. 198 | 199 | 本项目采用 **[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/)** 协议许可。 200 | 禁止将本项目或其衍生作品用于任何商业用途。 201 | 202 | -------------------------------------------------------------------------------- /src/components/dashboard/CurrentAccount.tsx: -------------------------------------------------------------------------------- 1 | import { CheckCircle, Mail } from 'lucide-react'; 2 | import { Account } from '../../types/account'; 3 | import { formatTimeRemaining } from '../../utils/format'; 4 | 5 | interface CurrentAccountProps { 6 | account: Account | null; 7 | onSwitch?: () => void; 8 | } 9 | 10 | import { useTranslation } from 'react-i18next'; 11 | 12 | function CurrentAccount({ account, onSwitch }: CurrentAccountProps) { 13 | const { t } = useTranslation(); 14 | if (!account) { 15 | return ( 16 |
17 |

18 | 19 | {t('dashboard.current_account')} 20 |

21 |
22 | {t('dashboard.no_active_account')} 23 |
24 |
25 | ); 26 | } 27 | 28 | const geminiModel = account.quota?.models.find(m => m.name.toLowerCase().includes('gemini')); 29 | const claudeModel = account.quota?.models.find(m => m.name.toLowerCase().includes('claude')); 30 | 31 | return ( 32 |
33 |

34 | 35 | {t('dashboard.current_account')} 36 |

37 | 38 |
39 |
40 | 41 | {account.email} 42 |
43 | 44 | {/* Gemini 配额 */} 45 | {geminiModel && ( 46 |
47 |
48 | Gemini 3 Pro 49 |
50 | 51 | {geminiModel.reset_time ? `R: ${formatTimeRemaining(geminiModel.reset_time)}` : t('common.unknown')} 52 | 53 | = 50 ? 'text-green-600 dark:text-green-400' : 54 | geminiModel.percentage >= 20 ? 'text-orange-600 dark:text-orange-400' : 'text-red-600 dark:text-red-400' 55 | }`}> 56 | {geminiModel.percentage}% 57 | 58 |
59 |
60 |
61 |
= 50 ? 'bg-gradient-to-r from-green-400 to-green-500' : 63 | geminiModel.percentage >= 20 ? 'bg-gradient-to-r from-orange-400 to-orange-500' : 64 | 'bg-gradient-to-r from-red-400 to-red-500' 65 | }`} 66 | style={{ width: `${geminiModel.percentage}%` }} 67 | >
68 |
69 |
70 | )} 71 | 72 | {/* Claude 配额 */} 73 | {claudeModel && ( 74 |
75 |
76 | Claude 4.5 77 |
78 | 79 | {claudeModel.reset_time ? `R: ${formatTimeRemaining(claudeModel.reset_time)}` : t('common.unknown')} 80 | 81 | = 50 ? 'text-cyan-600 dark:text-cyan-400' : 82 | claudeModel.percentage >= 20 ? 'text-orange-600 dark:text-orange-400' : 'text-red-600 dark:text-red-400' 83 | }`}> 84 | {claudeModel.percentage}% 85 | 86 |
87 |
88 |
89 |
= 50 ? 'bg-gradient-to-r from-cyan-400 to-cyan-500' : 91 | claudeModel.percentage >= 20 ? 'bg-gradient-to-r from-orange-400 to-orange-500' : 92 | 'bg-gradient-to-r from-red-400 to-red-500' 93 | }`} 94 | style={{ width: `${claudeModel.percentage}%` }} 95 | >
96 |
97 |
98 | )} 99 |
100 | 101 | {onSwitch && ( 102 |
103 | 109 |
110 | )} 111 |
112 | ); 113 | } 114 | 115 | export default CurrentAccount; 116 | -------------------------------------------------------------------------------- /src-tauri/src/modules/oauth.rs: -------------------------------------------------------------------------------- 1 | use reqwest::Client; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | // Google OAuth 配置 5 | const CLIENT_ID: &str = "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com"; 6 | const CLIENT_SECRET: &str = "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"; 7 | const TOKEN_URL: &str = "https://oauth2.googleapis.com/token"; 8 | const USERINFO_URL: &str = "https://www.googleapis.com/oauth2/v2/userinfo"; 9 | const REDIRECT_URI: &str = "http://localhost:8888/oauth-callback"; 10 | const AUTH_URL: &str = "https://accounts.google.com/o/oauth2/v2/auth"; 11 | 12 | #[derive(Debug, Serialize, Deserialize)] 13 | pub struct TokenResponse { 14 | pub access_token: String, 15 | pub expires_in: i64, 16 | #[serde(default)] 17 | pub token_type: String, 18 | #[serde(default)] 19 | pub refresh_token: Option, 20 | } 21 | 22 | #[derive(Debug, Serialize, Deserialize)] 23 | pub struct UserInfo { 24 | pub email: String, 25 | pub name: Option, 26 | pub given_name: Option, 27 | pub family_name: Option, 28 | pub picture: Option, 29 | } 30 | 31 | impl UserInfo { 32 | /// 获取最佳的显示名称 33 | pub fn get_display_name(&self) -> Option { 34 | // 优先使用 name 35 | if let Some(name) = &self.name { 36 | if !name.trim().is_empty() { 37 | return Some(name.clone()); 38 | } 39 | } 40 | 41 | // 如果 name 为空,尝试组合 given_name 和 family_name 42 | match (&self.given_name, &self.family_name) { 43 | (Some(given), Some(family)) => Some(format!("{} {}", given, family)), 44 | (Some(given), None) => Some(given.clone()), 45 | (None, Some(family)) => Some(family.clone()), 46 | (None, None) => None, 47 | } 48 | } 49 | } 50 | 51 | 52 | /// 生成 OAuth 授权 URL 53 | pub fn get_auth_url(redirect_uri: &str) -> String { 54 | let scopes = vec![ 55 | "https://www.googleapis.com/auth/cloud-platform", 56 | "https://www.googleapis.com/auth/userinfo.email", 57 | "https://www.googleapis.com/auth/userinfo.profile", 58 | "https://www.googleapis.com/auth/cclog", 59 | "https://www.googleapis.com/auth/experimentsandconfigs" 60 | ].join(" "); 61 | 62 | let params = vec![ 63 | ("client_id", CLIENT_ID), 64 | ("redirect_uri", redirect_uri), 65 | ("response_type", "code"), 66 | ("scope", &scopes), 67 | ("access_type", "offline"), 68 | ("prompt", "consent"), 69 | ("include_granted_scopes", "true"), 70 | ]; 71 | 72 | let url = url::Url::parse_with_params(AUTH_URL, ¶ms).expect("无效的 Auth URL"); 73 | url.to_string() 74 | } 75 | 76 | /// 使用 Authorization Code 交换 Token 77 | pub async fn exchange_code(code: &str, redirect_uri: &str) -> Result { 78 | let client = Client::builder() 79 | .timeout(std::time::Duration::from_secs(15)) 80 | .build() 81 | .map_err(|e| format!("无法创建 HTTP 客户端: {}", e))?; 82 | 83 | let params = [ 84 | ("client_id", CLIENT_ID), 85 | ("client_secret", CLIENT_SECRET), 86 | ("code", code), 87 | ("redirect_uri", redirect_uri), 88 | ("grant_type", "authorization_code"), 89 | ]; 90 | 91 | let response = client 92 | .post(TOKEN_URL) 93 | .form(¶ms) 94 | .send() 95 | .await 96 | .map_err(|e| format!("Token 交换请求失败: {}", e))?; 97 | 98 | if response.status().is_success() { 99 | response.json::() 100 | .await 101 | .map_err(|e| format!("Token 解析失败: {}", e)) 102 | } else { 103 | let error_text = response.text().await.unwrap_or_default(); 104 | Err(format!("Token 交换失败: {}", error_text)) 105 | } 106 | } 107 | 108 | /// 使用 refresh_token 刷新 access_token 109 | pub async fn refresh_access_token(refresh_token: &str) -> Result { 110 | let client = Client::builder() 111 | .timeout(std::time::Duration::from_secs(15)) 112 | .build() 113 | .map_err(|e| format!("无法创建 HTTP 客户端: {}", e))?; 114 | 115 | let params = [ 116 | ("client_id", CLIENT_ID), 117 | ("client_secret", CLIENT_SECRET), 118 | ("refresh_token", refresh_token), 119 | ("grant_type", "refresh_token"), 120 | ]; 121 | 122 | crate::modules::logger::log_info("正在刷新 Token..."); 123 | 124 | let response = client 125 | .post(TOKEN_URL) 126 | .form(¶ms) 127 | .send() 128 | .await 129 | .map_err(|e| format!("刷新请求失败: {}", e))?; 130 | 131 | if response.status().is_success() { 132 | let token_data = response 133 | .json::() 134 | .await 135 | .map_err(|e| format!("刷新数据解析失败: {}", e))?; 136 | 137 | crate::modules::logger::log_info(&format!("Token 刷新成功!有效期: {} 秒", token_data.expires_in)); 138 | Ok(token_data) 139 | } else { 140 | let error_text = response.text().await.unwrap_or_default(); 141 | Err(format!("刷新失败: {}", error_text)) 142 | } 143 | } 144 | 145 | /// 获取用户信息 146 | pub async fn get_user_info(access_token: &str) -> Result { 147 | let client = Client::builder() 148 | .timeout(std::time::Duration::from_secs(15)) 149 | .build() 150 | .map_err(|e| format!("无法创建 HTTP 客户端: {}", e))?; 151 | 152 | let response = client 153 | .get(USERINFO_URL) 154 | .bearer_auth(access_token) 155 | .send() 156 | .await 157 | .map_err(|e| format!("用户信息请求失败: {}", e))?; 158 | 159 | if response.status().is_success() { 160 | response.json::() 161 | .await 162 | .map_err(|e| format!("用户信息解析失败: {}", e)) 163 | } else { 164 | let error_text = response.text().await.unwrap_or_default(); 165 | Err(format!("获取用户信息失败: {}", error_text)) 166 | } 167 | } 168 | 169 | /// 检查并在需要时刷新 Token 170 | /// 返回最新的 access_token 171 | pub async fn ensure_fresh_token( 172 | current_token: &crate::models::TokenData, 173 | ) -> Result { 174 | let now = chrono::Local::now().timestamp(); 175 | 176 | // 如果没有过期时间,或者还有超过 5 分钟有效期,直接返回 177 | if current_token.expiry_timestamp > now + 300 { 178 | return Ok(current_token.clone()); 179 | } 180 | 181 | // 需要刷新 182 | crate::modules::logger::log_info("Token 即将过期,正在刷新..."); 183 | let response = refresh_access_token(¤t_token.refresh_token).await?; 184 | 185 | // 构造新 TokenData 186 | Ok(crate::models::TokenData { 187 | access_token: response.access_token, 188 | refresh_token: current_token.refresh_token.clone(), // 刷新时不一定会返回新的 refresh_token 189 | expires_in: response.expires_in, 190 | expiry_timestamp: now + response.expires_in, 191 | email: current_token.email.clone(), 192 | token_type: "Bearer".to_string(), 193 | }) 194 | } 195 | -------------------------------------------------------------------------------- /src/locales/zh.json: -------------------------------------------------------------------------------- 1 | { 2 | "common": { 3 | "loading": "加载中...", 4 | "save": "保存", 5 | "saved": "保存成功", 6 | "cancel": "取消", 7 | "confirm": "确认", 8 | "delete": "删除", 9 | "edit": "编辑", 10 | "refresh": "刷新", 11 | "refreshing": "刷新中...", 12 | "export": "导出", 13 | "import": "导入", 14 | "success": "成功", 15 | "error": "错误", 16 | "unknown": "未知", 17 | "warning": "警告", 18 | "info": "提示", 19 | "details": "详情", 20 | "clear": "清除", 21 | "prev_page": "上一页", 22 | "next_page": "下一页", 23 | "pagination_info": "显示第 {{start}} 到 {{end}} 条,共 {{total}} 条" 24 | }, 25 | "nav": { 26 | "dashboard": "仪表盘", 27 | "accounts": "账号管理", 28 | "settings": "设置" 29 | }, 30 | "dashboard": { 31 | "hello": "你好, 用户 👋", 32 | "refresh_quota": "刷新配额", 33 | "refreshing": "刷新中...", 34 | "total_accounts": "总账号数", 35 | "avg_gemini": "Gemini 平均配额", 36 | "avg_claude": "Claude 平均配额", 37 | "low_quota_accounts": "低配额账号", 38 | "quota_sufficient": "✓ 配额充足", 39 | "quota_low": "⚠ 配额较低", 40 | "quota_desc": "配额 < 20%", 41 | "current_account": "当前账号", 42 | "switch_account": "切换账号", 43 | "no_active_account": "暂无活跃账号", 44 | "best_accounts": "最佳账号推荐", 45 | "best_account_recommendation": "最佳账号推荐", 46 | "switch_best": "一键切换最佳", 47 | "switch_successfully": "一键切换最佳", 48 | "view_all_accounts": "查看所有账号", 49 | "export_data": "导出账号数据", 50 | "for_gemini": "用于 Gemini", 51 | "for_claude": "用于 Claude", 52 | "toast": { 53 | "switch_success": "切换成功!", 54 | "switch_error": "切换账号失败", 55 | "refresh_success": "配额刷新成功", 56 | "refresh_error": "刷新失败", 57 | "export_no_accounts": "没有可导出的账号", 58 | "export_success": "导出成功! 文件已保存至: {{path}}", 59 | "export_error": "导出失败" 60 | } 61 | }, 62 | "accounts": { 63 | "search_placeholder": "搜索邮箱...", 64 | "all": "全部", 65 | "available": "可用", 66 | "low_quota": "低配额", 67 | "add_account": "添加账号", 68 | "refresh_all": "刷新所有", 69 | "refresh_selected": "刷新 ({{count}})", 70 | "export_selected": "导出 ({{count}})", 71 | "delete_selected": "删除 ({{count}})", 72 | "current": "当前", 73 | "current_badge": "当前", 74 | "forbidden": "403", 75 | "forbidden_badge": "403", 76 | "forbidden_tooltip": "API 返回 403 Forbidden,账号无权使用 Gemini Code Assist", 77 | "forbidden_msg": "账号无权限,已跳过自动刷新", 78 | "no_data": "无数据", 79 | "last_used": "最后使用", 80 | "reset_time": "重置时间", 81 | "switch_to": "切换到此账号", 82 | "actions": "操作", 83 | "details": { 84 | "title": "配额详情" 85 | }, 86 | "add": { 87 | "title": "添加新账号", 88 | "tabs": { 89 | "oauth": "OAuth 授权", 90 | "token": "Refresh Token", 91 | "import": "从数据库导入" 92 | }, 93 | "oauth": { 94 | "recommend": "推荐方式", 95 | "desc": "将打开默认浏览器进行 Google 登录授权,自动获取并保存 Token。", 96 | "btn_start": "开始 OAuth 授权", 97 | "btn_waiting": "正在等待授权...", 98 | "copy_link": "复制授权链接" 99 | }, 100 | "token": { 101 | "label": "Refresh Token", 102 | "placeholder": "请在此处粘贴您的 Refresh Token...", 103 | "hint": "提示:粘贴 Token 后点击“确认添加”,系统将自动匹配账户信息。" 104 | }, 105 | "import": { 106 | "scheme_a": "方案 A: 从当前 IDE 数据库", 107 | "scheme_a_desc": "自动从本地 Antigravity 数据库读取当前登录的账号信息。", 108 | "btn_db": "导入当前登录账号", 109 | "or": "或者", 110 | "scheme_b": "方案 B: 从 V1 版本备份", 111 | "scheme_b_desc": "扫描 ~/.antigravity-agent 目录,批量导入旧版本的账号数据。", 112 | "btn_v1": "从 V1 备份批量导入" 113 | }, 114 | "btn_cancel": "取消", 115 | "btn_confirm": "确认添加", 116 | "status": { 117 | "error_token": "请填写 Refresh Token" 118 | } 119 | }, 120 | "table": { 121 | "email": "邮箱", 122 | "quota": "模型配额", 123 | "last_used": "最后使用", 124 | "actions": "操作" 125 | }, 126 | "empty": { 127 | "title": "暂无账号", 128 | "desc": "点击上方\"添加账号\"按钮添加第一个账号" 129 | }, 130 | "views": { 131 | "list": "列表视图", 132 | "grid": "网格视图" 133 | }, 134 | "dialog": { 135 | "add_title": "添加新账号", 136 | "batch_delete_title": "批量删除确认", 137 | "delete_title": "删除确认", 138 | "batch_delete_msg": "确定要删除选中的 {{count}} 个账号吗?此操作无法撤销。", 139 | "delete_msg": "确定要删除这个账号吗?此操作无法撤销。", 140 | "refresh_title": "刷新配额", 141 | "batch_refresh_title": "批量刷新", 142 | "refresh_msg": "确定要刷新当前账号的配额吗?", 143 | "batch_refresh_msg": "确定要刷新选中的 {{count}} 个账号的配额吗?这可能需要一些时间。" 144 | } 145 | }, 146 | "settings": { 147 | "save": "保存设置", 148 | "tabs": { 149 | "general": "通用", 150 | "account": "账号", 151 | "advanced": "高级", 152 | "about": "关于" 153 | }, 154 | "general": { 155 | "title": "通用设置", 156 | "language": "语言", 157 | "theme": "主题", 158 | "theme_light": "浅色", 159 | "theme_dark": "深色", 160 | "theme_system": "跟随系统" 161 | }, 162 | "account": { 163 | "title": "账号设置", 164 | "auto_refresh": "自动刷新配额", 165 | "auto_refresh_desc": "定期自动刷新所有账号的配额信息", 166 | "refresh_interval": "刷新间隔(分钟)", 167 | "auto_sync": "自动获取当前账号", 168 | "auto_sync_desc": "定期自动同步当前活跃账号信息", 169 | "sync_interval": "同步间隔(秒)" 170 | }, 171 | "advanced": { 172 | "title": "高级设置", 173 | "export_path": "默认导出路径", 174 | "export_path_placeholder": "未设置 (每次询问)", 175 | "default_export_path_desc": "设置后,导出文件将直接保存到该目录,不再弹出选择框", 176 | "select_btn": "选择", 177 | "open_btn": "打开", 178 | "data_dir": "数据目录", 179 | "data_dir_desc": "账号数据和配置文件的存储位置", 180 | "logs_title": "日志维护", 181 | "logs_desc": "清理应用产生的日志缓存文件,不会影响账号数据。", 182 | "clear_logs": "清理日志缓存", 183 | "clear_logs_title": "清理日志确认", 184 | "clear_logs_msg": "确定要清理所有日志缓存文件吗?", 185 | "logs_cleared": "日志缓存已清理" 186 | }, 187 | "about": { 188 | "title": "关于", 189 | "version": "应用版本", 190 | "tech_stack": "技术栈", 191 | "author": "作者", 192 | "wechat": "微信公众号", 193 | "github": "开源地址", 194 | "copyright": "Copyright © 2025 Antigravity. All rights reserved." 195 | } 196 | }, 197 | "tray": { 198 | "current": "当前", 199 | "quota": "额度", 200 | "switch_next": "切换下一个账号", 201 | "refresh_current": "刷新当前账号额度", 202 | "show_window": "显示主窗口", 203 | "quit": "退出应用 (Exit)", 204 | "no_account": "无账号", 205 | "unknown_quota": "未知 (点击刷新)", 206 | "forbidden": "账号被封禁" 207 | } 208 | } -------------------------------------------------------------------------------- /README_EN.md: -------------------------------------------------------------------------------- 1 | # Antigravity Tools 🚀 2 | 3 |
4 | Antigravity Logo 5 | 6 |

Professional Account Management for AI Services

7 |

Manage your Gemini / Claude accounts with ease. Unlimited Possibilities.

8 | 9 |

10 | 11 | Version 12 | 13 | Tauri 14 | React 15 | License 16 |

17 | 18 |

19 | 📥 Download • 20 | ✨ Features • 21 | 🆚 Comparison 22 |

23 | 24 |

25 | 🇨🇳 简体中文 | 26 | 🇺🇸 English 27 |

28 |
29 | 30 | --- 31 | 32 |
33 | Antigravity Dark Mode 34 |

(Deep Dark Mode: Increased productivity)

35 |
36 | 37 | ## 🎨 Gallery 38 | 39 |
40 | 41 | | **Light Mode** | **Dark Mode** | 42 | | :---: | :---: | 43 | | | | 44 | | **Dashboard** | **Accounts** | 45 | 46 | | | | 47 | | **List View** | **Settings** | 48 | 49 |
50 | 51 | --- 52 | 53 | **Antigravity Tools** is a **modern account management tool** built for AI developers and power users. 54 | 55 | As the 2.0 rewrite of [Antigravity Manager](https://github.com/lbjlaq/Antigravity-Manager), it leverages the high-performance **[Tauri v2](https://v2.tauri.app/)** + **[React](https://react.dev/)** stack, evolving from a heavy Python GUI into a lightweight, blazing-fast native application. 56 | 57 | It helps you effortlessly manage dozens of **Google Gemini** and **Claude 3.5** accounts, monitoring Quotas in real-time, and intelligently switching accounts when quotas are exhausted, enabling an "unlimited" AI experience. 58 | 59 | > ⚠️ **Note**: The project repository URL remains unchanged at [lbjlaq/Antigravity-Manager](https://github.com/lbjlaq/Antigravity-Manager). 60 | > 61 | > **Looking for v1.0?** 62 | > The source code for v1.0 (Python/Flet) has been archived in the [v1 branch](https://github.com/lbjlaq/Antigravity-Manager/tree/v1). Switch branches to view or maintain the legacy version. 63 | 64 | ## 🆚 Why v2.0? (Comparison) 65 | 66 | | Feature | 🐢 v1.0 (Legacy) | 🚀 v2.0 (New) | Improvement | 67 | | :--- | :--- | :--- | :--- | 68 | | **Core Tech** | Python + Flet | **Rust (Tauri)** + **React** | **Performance Leap** | 69 | | **Bundle Size** | ~80 MB | **~10 MB** | **87% Smaller** | 70 | | **Startup Time** | Slow (Interpreted) | **Instant** (Native Binary) | **Blazing Fast** | 71 | | **Memory Usage** | High (>200MB) | **Tiny** (<50MB) | **Efficient** | 72 | | **UI/UX** | Basic Material | **Modern Glassmorphism** | **Beautiful** | 73 | | **Security** | Plain text / Simple obfuscation | **Local JSON Storage** | **Transparent & Controllable** | 74 | | **Extensibility** | Hard (Python dependency hell) | **Easy** (Standard Web Tech) | **Rich Ecosystem** | 75 | 76 | ## ✨ Key Features 77 | 78 | ### 📊 Dashboard 79 | - **Global Overview**: Real-time display of total accounts, average quota for each model, health status at a glance. 80 | - **Smart Recommendation**: Automatically filters the "Best Account" with the most available quota, supports one-click switching to always use optimal resources. 81 | - **Status Monitoring**: Real-time highlighting of accounts with low quota alerts to avoid development interruptions. 82 | 83 | ### 👥 Account Management 84 | - **Multi-channel Import**: 85 | - 🔥 **OAuth Authorization**: Supports browser-based Google login authorization to automatically acquire Tokens (Recommended). 86 | - 📋 **Manual Add**: Supports direct pasting of Refresh Tokens. 87 | - 📂 **V1 Migration**: Supports scanning and batch importing old data from v1 version (`~/.antigravity-agent`). 88 | - 🔄 **Local Sync**: Supports automatically reading and importing currently logged-in accounts from IDE (Cursor/Windsurf) local database. 89 | - **Batch Operations**: Batch refresh quota, batch export backup (JSON), batch delete. 90 | - **Search & Filter**: Supports fast retrieval by email keywords, managing dozens of accounts with ease. 91 | 92 | ### 🔄 Quota Sync 93 | - **Auto Refresh**: Configurable background automatic polling for latest quota information of all accounts. 94 | - **Token Keep-alive**: Built-in Token auto-refresh mechanism, auto-renew upon expiration to ensure connection validity. 95 | - **Precise Display**: Clearly displays specific remaining percentage and reset time for Gemini / Claude models. 96 | 97 | ### 🛠️ System Integration 98 | - **Tray Resident**: Minimized to system tray, saving taskbar space, running silently in background. 99 | - **Quick Actions**: Tray menu supports one-click viewing of current quota and quick switching to next available account. 100 | - **Secure Storage**: Uses local JSON format storage, all Token data is saved only on user device, never uploaded to cloud. 101 | 102 | ### ⚙️ Settings 103 | - **Internationalization**: Native support for **Simplified Chinese** / **English** real-time switching. 104 | - **Theme Adaptation**: Perfectly adapts to system Dark / Light mode, eye-friendly for night use. 105 | - **Data Management**: Supports custom data export path and one-click log cache cleaning. 106 | 107 | ## 🛠️ Tech Stack 108 | 109 | Built with cutting-edge modern tech stack, ensuring high performance and maintainability: 110 | 111 | | Module | Tech Stack | Description | 112 | | :--- | :--- | :--- | 113 | | **Frontend** | React 18 + TypeScript | UI Construction & Logic | 114 | | **UI Framework** | TailwindCSS + DaisyUI | Modern Atomic CSS Library | 115 | | **Backend** | Tauri v2 (Rust) | High-performance System Interaction | 116 | | **Storage** | Local JSON | Local Config & Data Storage | 117 | | **State** | Zustand | Lightweight Global State Management | 118 | | **Network** | Reqwest (Async) | Async Network Requests | 119 | 120 | ## 📦 Installation & Run 121 | 122 | ### 📥 Download 123 | 124 | Visit the [Releases Page](https://github.com/lbjlaq/Antigravity-Manager/releases) to download the installer for your system: 125 | 126 | - **macOS**: Supports Intel (`.dmg`) and Apple Silicon (`.dmg`) 127 | - **Windows**: `.exe` Installer 128 | - **Linux**: `.deb` or `.AppImage` *(Theoretical support, untested, feedback welcome)* 129 | 130 | ### 💻 Development 131 | 132 | If you're a developer and want to contribute: 133 | 134 | ```bash 135 | # 1. Clone project 136 | git clone https://github.com/lbjlaq/antigravity-tools.git 137 | 138 | # 2. Install dependencies 139 | npm install 140 | 141 | # 3. Start dev server (Frontend + Backend) 142 | npm run tauri dev 143 | ``` 144 | 145 | ### 🏗️ Build 146 | 147 | ```bash 148 | # Build Universal macOS App (Intel & Apple Silicon) 149 | npm run build:universal 150 | ``` 151 | 152 | ## 👤 Author 153 | 154 | **Ctrler** 155 | 156 | - 💬 WeChat: `Ctrler` 157 | - 🐙 GitHub: [@lbjlaq](https://github.com/lbjlaq) 158 | 159 | ## 📄 License 160 | 161 | Copyright © 2025 Antigravity. All rights reserved. 162 | 163 | This project is licensed under the **[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/)** license. 164 | Commercial use of this project or its derivatives is strictly prohibited. 165 | -------------------------------------------------------------------------------- /src/components/common/Pagination.tsx: -------------------------------------------------------------------------------- 1 | import { ChevronLeft, ChevronRight } from 'lucide-react'; 2 | import { useTranslation } from 'react-i18next'; 3 | 4 | interface PaginationProps { 5 | currentPage: number; 6 | totalPages: number; 7 | onPageChange: (page: number) => void; 8 | totalItems: number; 9 | itemsPerPage: number; 10 | } 11 | 12 | function Pagination({ currentPage, totalPages, onPageChange, totalItems, itemsPerPage }: PaginationProps) { 13 | const { t } = useTranslation(); 14 | 15 | if (totalPages <= 1) return null; 16 | 17 | // 计算显示的页码范围 (最多显示 5 个页码) 18 | let startPage = Math.max(1, currentPage - 2); 19 | let endPage = Math.min(totalPages, startPage + 4); 20 | 21 | if (endPage - startPage < 4) { 22 | startPage = Math.max(1, endPage - 4); 23 | } 24 | 25 | const pages = []; 26 | for (let i = startPage; i <= endPage; i++) { 27 | pages.push(i); 28 | } 29 | 30 | const startIndex = (currentPage - 1) * itemsPerPage + 1; 31 | const endIndex = Math.min(currentPage * itemsPerPage, totalItems); 32 | 33 | return ( 34 |
35 | {/* Mobile View */} 36 |
37 | 47 | 57 |
58 | 59 | {/* Desktop View */} 60 |
61 |
62 |

63 | {t('common.pagination_info', { start: startIndex, end: endIndex, total: totalItems })} 64 |

65 |
66 |
67 | 136 |
137 |
138 |
139 | ); 140 | } 141 | 142 | export default Pagination; 143 | -------------------------------------------------------------------------------- /src/locales/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "common": { 3 | "loading": "Loading...", 4 | "save": "Save", 5 | "saved": "Saved successfully", 6 | "cancel": "Cancel", 7 | "confirm": "Confirm", 8 | "delete": "Delete", 9 | "edit": "Edit", 10 | "refresh": "Refresh", 11 | "refreshing": "Refreshing...", 12 | "export": "Export", 13 | "import": "Import", 14 | "success": "Success", 15 | "error": "Error", 16 | "unknown": "Unknown", 17 | "warning": "Warning", 18 | "info": "Info", 19 | "details": "Details", 20 | "clear": "Clear", 21 | "prev_page": "Previous", 22 | "next_page": "Next", 23 | "pagination_info": "Showing {{start}} to {{end}} of {{total}} entries" 24 | }, 25 | "nav": { 26 | "dashboard": "Dashboard", 27 | "accounts": "Accounts", 28 | "settings": "Settings" 29 | }, 30 | "dashboard": { 31 | "hello": "Hello, User 👋", 32 | "refresh_quota": "Refresh Quota", 33 | "refreshing": "Refreshing...", 34 | "total_accounts": "Total Accounts", 35 | "avg_gemini": "Avg. Gemini Quota", 36 | "avg_claude": "Avg. Claude Quota", 37 | "low_quota_accounts": "Low Quota Accounts", 38 | "quota_sufficient": "Quota Sufficient", 39 | "quota_low": "Low Quota", 40 | "quota_desc": "Quota < 20%", 41 | "current_account": "Current Account", 42 | "switch_account": "Switch Account", 43 | "no_active_account": "No Active Account", 44 | "best_accounts": "Best Accounts", 45 | "best_account_recommendation": "Best Account", 46 | "switch_best": "Switch to Best", 47 | "switch_successfully": "Switch to Best", 48 | "view_all_accounts": "View All Accounts", 49 | "export_data": "Export Data", 50 | "for_gemini": "For Gemini", 51 | "for_claude": "For Claude", 52 | "toast": { 53 | "switch_success": "Switch successful!", 54 | "switch_error": "Switch account failed", 55 | "refresh_success": "Quota refresh successful", 56 | "refresh_error": "Refresh failed", 57 | "export_no_accounts": "No accounts to export", 58 | "export_success": "Export successful! File saved to: {{path}}", 59 | "export_error": "Export failed" 60 | } 61 | }, 62 | "accounts": { 63 | "search_placeholder": "Search email...", 64 | "all": "All", 65 | "available": "Available", 66 | "low_quota": "Low Quota", 67 | "add_account": "Add Account", 68 | "refresh_all": "Refresh All", 69 | "refresh_selected": "Refresh ({{count}})", 70 | "export_selected": "Export ({{count}})", 71 | "delete_selected": "Delete ({{count}})", 72 | "current": "Current", 73 | "current_badge": "Current", 74 | "forbidden": "403", 75 | "forbidden_badge": "403", 76 | "forbidden_tooltip": "API returned 403 Forbidden, account has no permission for Gemini Code Assist", 77 | "forbidden_msg": "Forbidden, skip auto-refresh", 78 | "no_data": "No Data", 79 | "last_used": "Last Used", 80 | "reset_time": "Reset Time", 81 | "switch_to": "Switch to this account", 82 | "actions": "Actions", 83 | "details": { 84 | "title": "Quota Details" 85 | }, 86 | "add": { 87 | "title": "Add Account", 88 | "tabs": { 89 | "oauth": "OAuth", 90 | "token": "Refresh Token", 91 | "import": "Import DB" 92 | }, 93 | "oauth": { 94 | "recommend": "Recommended", 95 | "desc": "Opens default browser for Google login to auto-fetch and save Token.", 96 | "btn_start": "Start OAuth", 97 | "btn_waiting": "Waiting for auth...", 98 | "copy_link": "Copy Auth Link" 99 | }, 100 | "token": { 101 | "label": "Refresh Token", 102 | "placeholder": "Paste your Refresh Token here...", 103 | "hint": "Tip: Paste Token and click 'Confirm' to auto-match account info." 104 | }, 105 | "import": { 106 | "scheme_a": "Plan A: From IDE DB", 107 | "scheme_a_desc": "Auto-read current logged-in account from local Antigravity DB.", 108 | "btn_db": "Import Current Account", 109 | "or": "OR", 110 | "scheme_b": "Plan B: From V1 Backup", 111 | "scheme_b_desc": "Scan ~/.antigravity-agent for V1 account data.", 112 | "btn_v1": "Batch Import V1" 113 | }, 114 | "btn_cancel": "Cancel", 115 | "btn_confirm": "Confirm", 116 | "status": { 117 | "error_token": "Please enter Refresh Token" 118 | } 119 | }, 120 | "table": { 121 | "email": "Email", 122 | "quota": "Model Quota", 123 | "last_used": "Last Used", 124 | "actions": "Actions" 125 | }, 126 | "empty": { 127 | "title": "No Accounts", 128 | "desc": "Click the \"Add Account\" button above to add your first account" 129 | }, 130 | "views": { 131 | "list": "List View", 132 | "grid": "Grid View" 133 | }, 134 | "dialog": { 135 | "add_title": "Add Account", 136 | "batch_delete_title": "Batch Delete Confirmation", 137 | "delete_title": "Delete Confirmation", 138 | "batch_delete_msg": "Are you sure you want to delete the selected {{count}} accounts? This action cannot be undone.", 139 | "delete_msg": "Are you sure you want to delete this account? This action cannot be undone.", 140 | "refresh_title": "Refresh Quota", 141 | "batch_refresh_title": "Batch Refresh", 142 | "refresh_msg": "Are you sure you want to refresh the quota for the current account?", 143 | "batch_refresh_msg": "Are you sure you want to refresh quotas for the selected {{count}} accounts? This may take some time." 144 | } 145 | }, 146 | "settings": { 147 | "save": "Save Settings", 148 | "tabs": { 149 | "general": "General", 150 | "account": "Account", 151 | "advanced": "Advanced", 152 | "about": "About" 153 | }, 154 | "general": { 155 | "title": "General Settings", 156 | "language": "Language", 157 | "theme": "Theme", 158 | "theme_light": "Light", 159 | "theme_dark": "Dark", 160 | "theme_system": "System" 161 | }, 162 | "account": { 163 | "title": "Account Settings", 164 | "auto_refresh": "Auto Refresh Quota", 165 | "auto_refresh_desc": "Automatically refresh quota information for all accounts periodically", 166 | "refresh_interval": "Refresh Interval (minutes)", 167 | "auto_sync": "Auto Sync Current Account", 168 | "auto_sync_desc": "Automatically sync current active account information periodically", 169 | "sync_interval": "Sync Interval (seconds)" 170 | }, 171 | "advanced": { 172 | "title": "Advanced Settings", 173 | "export_path": "Default Export Path", 174 | "export_path_placeholder": "Not set (Ask every time)", 175 | "default_export_path_desc": "Files will be saved directly to this folder without asking", 176 | "select_btn": "Select", 177 | "open_btn": "Open", 178 | "data_dir": "Data Directory", 179 | "data_dir_desc": "Account data and config file location", 180 | "logs_title": "Logs Maintenance", 181 | "logs_desc": "Clear log cache files. Does not affect account data.", 182 | "clear_logs": "Clear Logs Cache", 183 | "clear_logs_title": "Clear Logs Confirmation", 184 | "clear_logs_msg": "Are you sure you want to clear all log cache files?", 185 | "logs_cleared": "Logs cache cleared" 186 | }, 187 | "about": { 188 | "title": "About", 189 | "version": "App Version", 190 | "tech_stack": "Tech Stack", 191 | "author": "Author", 192 | "wechat": "WeChat", 193 | "github": "GitHub", 194 | "copyright": "Copyright © 2025 Antigravity. All rights reserved." 195 | } 196 | }, 197 | "tray": { 198 | "current": "Current", 199 | "quota": "Quota", 200 | "switch_next": "Switch to Next Account", 201 | "refresh_current": "Refresh Current Quota", 202 | "show_window": "Show Main Window", 203 | "quit": "Quit Application", 204 | "no_account": "No Account", 205 | "unknown_quota": "Unknown (Click to Refresh)", 206 | "forbidden": "Account Forbidden" 207 | } 208 | } --------------------------------------------------------------------------------