├── src-tauri ├── 2 ├── build.rs ├── icons │ ├── 32x32.png │ ├── 64x64.png │ ├── icon.icns │ ├── icon.ico │ ├── icon.png │ ├── 128x128.png │ ├── 128x128@2x.png │ ├── StoreLogo.png │ ├── installer.ico │ ├── 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 │ └── 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 ├── img │ ├── Header.bmp │ └── Sidebar.bmp ├── .gitignore ├── src │ ├── main.rs │ ├── theme_apply.rs │ ├── lib.rs │ └── windows_themes │ │ └── mod.rs ├── capabilities │ ├── desktop.json │ └── default.json ├── Appx │ └── Tuyang.AutoThemeMode.yaml ├── Cargo.toml └── tauri.conf.json ├── winset.png ├── .vscode ├── extensions.json └── launch.json ├── target └── rust-analyzer │ └── flycheck0 │ └── stderr ├── src ├── typings.d.ts ├── vite-env.d.ts ├── svg.d.ts ├── mod │ ├── openStoreRating.ts │ ├── applyTheme.ts │ ├── adjustTime.ts │ ├── PageTitle.ts │ ├── Deviation.tsx │ ├── Crontab.ts │ ├── utils │ │ ├── path.ts │ │ └── tauri-file.ts │ ├── searchCiti.tsx │ ├── WindowCode.ts │ ├── ThemeConfig.ts │ ├── RatingPrompt.tsx │ ├── DataSave.ts │ ├── sociti.ts │ ├── update.ts │ └── Mainoption.tsx ├── main.tsx ├── Type.ts ├── com │ ├── ThemeThumb.tsx │ └── ThemeSelector.tsx ├── assets │ ├── closed.svg │ ├── min.svg │ ├── logo.svg │ └── StoreLogo.svg ├── language │ ├── zh-HK.json │ ├── zh-CN.json │ ├── ja-JP.json │ ├── en.json │ ├── ru.json │ ├── es-ES.json │ └── index.tsx ├── doc.tsx ├── Content.tsx ├── App.css ├── updates.tsx ├── TitleBar.tsx └── App.tsx ├── tsconfig.node.json ├── .github └── ISSUE_TEMPLATE │ ├── feedback.md │ └── bug_report.md ├── .gitignore ├── index.html ├── tsconfig.json ├── vite.config.ts ├── LICENSE ├── package.json ├── public ├── vite.svg └── tauri.svg ├── README.md ├── PRIVACY.md └── README-English.md /src-tauri/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | tauri_build::build(); 3 | } 4 | -------------------------------------------------------------------------------- /winset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuyangJs/Windows_AutoTheme/HEAD/winset.png -------------------------------------------------------------------------------- /src-tauri/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuyangJs/Windows_AutoTheme/HEAD/src-tauri/icons/32x32.png -------------------------------------------------------------------------------- /src-tauri/icons/64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuyangJs/Windows_AutoTheme/HEAD/src-tauri/icons/64x64.png -------------------------------------------------------------------------------- /src-tauri/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuyangJs/Windows_AutoTheme/HEAD/src-tauri/icons/icon.icns -------------------------------------------------------------------------------- /src-tauri/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuyangJs/Windows_AutoTheme/HEAD/src-tauri/icons/icon.ico -------------------------------------------------------------------------------- /src-tauri/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuyangJs/Windows_AutoTheme/HEAD/src-tauri/icons/icon.png -------------------------------------------------------------------------------- /src-tauri/img/Header.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuyangJs/Windows_AutoTheme/HEAD/src-tauri/img/Header.bmp -------------------------------------------------------------------------------- /src-tauri/img/Sidebar.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuyangJs/Windows_AutoTheme/HEAD/src-tauri/img/Sidebar.bmp -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"] 3 | } 4 | -------------------------------------------------------------------------------- /src-tauri/2: -------------------------------------------------------------------------------- 1 | 2 | up to date in 914ms 3 | 4 | 96 packages are looking for funding 5 | run `npm fund` for details 6 | -------------------------------------------------------------------------------- /src-tauri/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuyangJs/Windows_AutoTheme/HEAD/src-tauri/icons/128x128.png -------------------------------------------------------------------------------- /src-tauri/icons/128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuyangJs/Windows_AutoTheme/HEAD/src-tauri/icons/128x128@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/StoreLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuyangJs/Windows_AutoTheme/HEAD/src-tauri/icons/StoreLogo.png -------------------------------------------------------------------------------- /src-tauri/icons/installer.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuyangJs/Windows_AutoTheme/HEAD/src-tauri/icons/installer.ico -------------------------------------------------------------------------------- /src-tauri/icons/Square30x30Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuyangJs/Windows_AutoTheme/HEAD/src-tauri/icons/Square30x30Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square44x44Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuyangJs/Windows_AutoTheme/HEAD/src-tauri/icons/Square44x44Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square71x71Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuyangJs/Windows_AutoTheme/HEAD/src-tauri/icons/Square71x71Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square89x89Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuyangJs/Windows_AutoTheme/HEAD/src-tauri/icons/Square89x89Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square107x107Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuyangJs/Windows_AutoTheme/HEAD/src-tauri/icons/Square107x107Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square142x142Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuyangJs/Windows_AutoTheme/HEAD/src-tauri/icons/Square142x142Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square150x150Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuyangJs/Windows_AutoTheme/HEAD/src-tauri/icons/Square150x150Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square284x284Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuyangJs/Windows_AutoTheme/HEAD/src-tauri/icons/Square284x284Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square310x310Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuyangJs/Windows_AutoTheme/HEAD/src-tauri/icons/Square310x310Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuyangJs/Windows_AutoTheme/HEAD/src-tauri/icons/ios/AppIcon-512@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuyangJs/Windows_AutoTheme/HEAD/src-tauri/icons/ios/AppIcon-20x20@1x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuyangJs/Windows_AutoTheme/HEAD/src-tauri/icons/ios/AppIcon-20x20@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuyangJs/Windows_AutoTheme/HEAD/src-tauri/icons/ios/AppIcon-20x20@3x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuyangJs/Windows_AutoTheme/HEAD/src-tauri/icons/ios/AppIcon-29x29@1x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuyangJs/Windows_AutoTheme/HEAD/src-tauri/icons/ios/AppIcon-29x29@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuyangJs/Windows_AutoTheme/HEAD/src-tauri/icons/ios/AppIcon-29x29@3x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuyangJs/Windows_AutoTheme/HEAD/src-tauri/icons/ios/AppIcon-40x40@1x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuyangJs/Windows_AutoTheme/HEAD/src-tauri/icons/ios/AppIcon-40x40@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuyangJs/Windows_AutoTheme/HEAD/src-tauri/icons/ios/AppIcon-40x40@3x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuyangJs/Windows_AutoTheme/HEAD/src-tauri/icons/ios/AppIcon-60x60@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuyangJs/Windows_AutoTheme/HEAD/src-tauri/icons/ios/AppIcon-60x60@3x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuyangJs/Windows_AutoTheme/HEAD/src-tauri/icons/ios/AppIcon-76x76@1x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuyangJs/Windows_AutoTheme/HEAD/src-tauri/icons/ios/AppIcon-76x76@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-20x20@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuyangJs/Windows_AutoTheme/HEAD/src-tauri/icons/ios/AppIcon-20x20@2x-1.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-29x29@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuyangJs/Windows_AutoTheme/HEAD/src-tauri/icons/ios/AppIcon-29x29@2x-1.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-40x40@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuyangJs/Windows_AutoTheme/HEAD/src-tauri/icons/ios/AppIcon-40x40@2x-1.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuyangJs/Windows_AutoTheme/HEAD/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png -------------------------------------------------------------------------------- /target/rust-analyzer/flycheck0/stderr: -------------------------------------------------------------------------------- 1 | warning: unused manifest key: http 2 | Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.74s 3 | -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuyangJs/Windows_AutoTheme/HEAD/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuyangJs/Windows_AutoTheme/HEAD/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuyangJs/Windows_AutoTheme/HEAD/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuyangJs/Windows_AutoTheme/HEAD/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuyangJs/Windows_AutoTheme/HEAD/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuyangJs/Windows_AutoTheme/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/tuyangJs/Windows_AutoTheme/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/tuyangJs/Windows_AutoTheme/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/tuyangJs/Windows_AutoTheme/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/tuyangJs/Windows_AutoTheme/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/tuyangJs/Windows_AutoTheme/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/tuyangJs/Windows_AutoTheme/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/tuyangJs/Windows_AutoTheme/HEAD/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuyangJs/Windows_AutoTheme/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/tuyangJs/Windows_AutoTheme/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 | Cargo.lock 9 | -------------------------------------------------------------------------------- /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 | tauri_app_lib::run() 6 | } 7 | -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | import { Window as appwin } from '@tauri-apps/api/window'; // 引入 appWindow 2 | import { Webview } from "@tauri-apps/api/webview"; 3 | 4 | declare global { 5 | interface Window { 6 | appWindow: appwin; 7 | Webview:Webview 8 | } 9 | } -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | declare module '*.svg' { 3 | import * as React from 'react'; 4 | 5 | export const ReactComponent: React.FC>; 6 | 7 | const content: string; 8 | export default content; 9 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feedback.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 意见反馈 3 | about: Share your thoughts and suggestions 4 | title: "[feedback]" 5 | labels: feedback 6 | assignees: '' 7 | --- 8 | 9 | **您的反馈** 10 | 11 | 请清晰简洁地描述您的意见或建议。 12 | 13 | **相关上下文** 14 | 15 | 如果适用,请提供与您的反馈相关的上下文、截图或其他参考资料。 16 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "type": "node-terminal", 5 | "name": "Devbug", 6 | "request": "launch", 7 | "command": "npm start", 8 | "cwd": "${workspaceFolder}" 9 | } 10 | ] 11 | } -------------------------------------------------------------------------------- /src-tauri/capabilities/desktop.json: -------------------------------------------------------------------------------- 1 | { 2 | "identifier": "desktop-capability", 3 | "platforms": [ 4 | "macOS", 5 | "windows", 6 | "linux" 7 | ], 8 | "windows": [ 9 | "main" 10 | ], 11 | "permissions": [ 12 | "autostart:default", 13 | "window-state:default" 14 | ] 15 | } -------------------------------------------------------------------------------- /src/svg.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module "*.svg?react" { 4 | import React from "react"; 5 | const ReactComponent: React.FC>; 6 | export default ReactComponent; 7 | } 8 | 9 | declare module "*.svg" { 10 | const src: string; 11 | export default src; 12 | } 13 | -------------------------------------------------------------------------------- /.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/extensions.json 17 | .idea 18 | .DS_Store 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | Cargo.lock 25 | package-lock.json 26 | src-tauri/Cargo.lock 27 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Tauri + React + Typescript 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src-tauri/Appx/Tuyang.AutoThemeMode.yaml: -------------------------------------------------------------------------------- 1 | # MSIX Conversion Accelerator 2 | 3 | PackageName: Auto Theme Mode 4 | 5 | PublisherName: Tuyang 6 | 7 | PackageVersion: 1.6.0.0 8 | 9 | EligibleForConversion: Yes 10 | 11 | ConversionStatus: Package Created. Need Validation 12 | 13 | Edition: Microsoft Windows 11 专业工作站版 14 | 15 | MinimumOSVersion: 25H2 16 | 17 | MinimumOSBuild: 26200.7171 18 | 19 | Architecture: 64 20 | 21 | MSIXConversionToolVersion: 1.2024.405.0 22 | 23 | AcceleratorVersion: 1.0.0 24 | 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug 反馈 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: '' 7 | --- 8 | 9 | **描述问题** 10 | 清晰简洁地描述问题是什么。 11 | 12 | **重现步骤** 13 | 复现行为的步骤: 14 | 1. 去到 '...' 15 | 2. 点击 '....' 16 | 3. 滚动到 '....' 17 | 4. 看到错误 18 | 19 | **预期行为** 20 | 清晰简洁地描述你期望的结果。 21 | 22 | **截图** 23 | 如果适用,添加屏幕截图以帮助解释您的问题。 24 | 25 | **环境信息(请填写以下信息):** 26 | - 程序版本 [例如 1.4.0] 27 | - 系统版本 [例如 Windows11 24H2] 28 | 29 | **其他内容** 30 | 在此添加关于问题的任何其他上下文。 31 | -------------------------------------------------------------------------------- /src/mod/openStoreRating.ts: -------------------------------------------------------------------------------- 1 | import { openUrl } from '@tauri-apps/plugin-opener'; 2 | // Microsoft Store应用ID 3 | const STORE_APP_ID = "9N7ND584TDV1"; 4 | 5 | type storePage = 'pdp' | 'review' | 'downloadsandupdates'; 6 | export const openStoreRating = async (page?: storePage) => { 7 | const type = page || 'pdp'; 8 | const url = `ms-windows-store://${type}/?ProductId=${STORE_APP_ID}`; 9 | try { 10 | await openUrl(url); 11 | } catch { 12 | console.error('开启本地商店失败,回退至 WebStore', url); 13 | await openUrl(`https://apps.microsoft.com/store/detail/${STORE_APP_ID}`); 14 | } 15 | }; 16 | 17 | -------------------------------------------------------------------------------- /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/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import App from "./App"; 4 | document.addEventListener('keydown', function (e) { 5 | if ((e.key === 'F5') || (e.ctrlKey && e.key === 'r')) { 6 | e.preventDefault(); // 禁止刷新 7 | } 8 | }); 9 | document.addEventListener('contextmenu', function (e) { 10 | const target = e.target; 11 | // @ts-ignore 如果目标是 input 元素且没有被禁用,则允许右键菜单 12 | if (target.tagName.toLowerCase() === 'input' && !target.disabled) { 13 | return; 14 | } 15 | // 其它情况(非 input 或 input 处于禁用状态)都阻止右键菜单 16 | e.preventDefault(); 17 | }); 18 | 19 | // 将浏览器控制台与日志流分离 20 | ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( 21 | 22 | 23 | , 24 | ); 25 | -------------------------------------------------------------------------------- /src/mod/applyTheme.ts: -------------------------------------------------------------------------------- 1 | import { invoke } from "@tauri-apps/api/core"; 2 | import { Theme } from "../com/ThemeSelector"; 3 | 4 | // 应用主题(只需要路径) 5 | let timer: any = null 6 | export async function applyTheme(themePath: string) { 7 | try { 8 | clearTimeout(timer) 9 | timer = setTimeout(async () => { 10 | const themesData = (await invoke('get_windows_themes')) as Theme[]; 11 | // 找到匹配的主题 12 | const theme = themesData.find(t => t.path === themePath); 13 | if(theme?.is_active) return 14 | await invoke('apply_theme', { themePath }); 15 | console.log('主题应用成功'); 16 | }, 1000); 17 | return true; 18 | } catch (error) { 19 | console.error('应用主题失败:', error); 20 | throw error; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/mod/adjustTime.ts: -------------------------------------------------------------------------------- 1 | export function adjustTime(timeStr: string, deltaMinutes: number): string { 2 | // 分割小时和分钟并转换为数字 3 | const [hours, minutes] = timeStr.split(':').map(Number); 4 | 5 | // 计算总分钟数并应用时间差 6 | let totalMinutes = hours * 60 + minutes + deltaMinutes; 7 | 8 | // 处理 24 小时制循环(包含负数情况) 9 | totalMinutes = ((totalMinutes % 1440) + 1440) % 1440; // 1440 = 24h * 60m 10 | 11 | // 计算新小时和分钟 12 | const newHours = Math.floor(totalMinutes / 60); 13 | const newMinutes = totalMinutes % 60; 14 | 15 | // 格式化输出,补前导零 16 | return `${String(newHours).padStart(2, '0')}:${String(newMinutes).padStart(2, '0')}`; 17 | } 18 | /* console.log(adjustTime('06:29', -10)); // 输出 "06:19" 19 | console.log(adjustTime('23:55', 10)); // 输出 "00:05" 20 | console.log(adjustTime('00:05', -10)); // 输出 "23:55" 21 | console.log(adjustTime('12:30', 150)); // 输出 "15:00" */ -------------------------------------------------------------------------------- /src/Type.ts: -------------------------------------------------------------------------------- 1 | 2 | // 在 src/Type.ts 中扩展 AppDataType 3 | export interface positionType { 4 | lat: number; 5 | lng: number 6 | tzid: string 7 | } 8 | export interface AppDataType { 9 | open: boolean; 10 | rcrl: boolean; 11 | city: { position?: positionType; name: string }; 12 | times: string[]; 13 | Autostart: boolean; 14 | language?: string; 15 | StartShow: boolean; 16 | Skipversion: string; 17 | winBgEffect: string; 18 | deviation: number; 19 | rawTime: string[]; 20 | // 新增评分相关字段 21 | ratingPrompt?: RatingPromptType 22 | //主题选项 23 | StyemTheme?: string[]; 24 | StyemThemeEnable?: boolean 25 | } 26 | 27 | export interface TimesProps { 28 | disabled?: boolean; 29 | } 30 | declare const App: React.FC; 31 | export default App; 32 | export interface RatingPromptType { 33 | lastPromptTime: number; // 上次提示时间戳 34 | promptCount: number; // 已提示次数 35 | neverShowAgain: boolean; // 不再提示 36 | } 37 | -------------------------------------------------------------------------------- /src/mod/PageTitle.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | 3 | /** 4 | * usePageTitle 5 | * 监听页面标题变化,并返回最新的标题 6 | */ 7 | export default function usePageTitle(): string { 8 | const [title, setTitle] = useState(document.title); 9 | 10 | useEffect(() => { 11 | // 获取 元素 12 | const titleElement = document.querySelector('title'); 13 | if (!titleElement) return; 14 | 15 | // 使用一个变量保存上一次的标题 16 | let lastTitle = document.title; 17 | 18 | const observer = new MutationObserver(() => { 19 | const newTitle = document.title; 20 | // 只有当标题发生了真正的变化时才更新状态 21 | if (newTitle !== lastTitle) { 22 | lastTitle = newTitle; 23 | setTitle(newTitle); 24 | } 25 | }); 26 | 27 | // 监听子节点变化 28 | observer.observe(titleElement, { childList: true }); 29 | 30 | return () => { 31 | observer.disconnect(); 32 | }; 33 | }, []); 34 | 35 | return title; 36 | } 37 | -------------------------------------------------------------------------------- /src/com/ThemeThumb.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useLocalImageUrl } from '../mod/utils/tauri-file'; 3 | 4 | const DEFAULT_LIGHT = 'https://gw.alipayobjects.com/zos/bmw-prod/f601048d-61c2-44d0-bf57-ca1afe7fd92e.svg'; 5 | 6 | type ThemeThumbProps = { 7 | wallpaperPath?: string; 8 | fallback?: string; // 可选:若不传使用默认图 9 | }; 10 | 11 | function ThemeThumbInner({ wallpaperPath, fallback }: ThemeThumbProps) { 12 | const { src } = useLocalImageUrl(wallpaperPath); 13 | const final = src || fallback || DEFAULT_LIGHT; 14 | 15 | return ( 16 | <img 17 | src={final} 18 | alt="" 19 | style={{ width: 120, height: 80, objectFit: 'cover', borderTopLeftRadius: 8, borderTopRightRadius: 8, display: 'block' }} 20 | draggable={false} 21 | /> 22 | ); 23 | } 24 | 25 | // memo:只有当 wallpaperPath 或 fallback 改变时才重新渲染 26 | export const ThemeThumb = React.memo(ThemeThumbInner, (prev, next) => { 27 | return prev.wallpaperPath === next.wallpaperPath && prev.fallback === next.fallback; 28 | }); 29 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | import svgr from "vite-plugin-svgr"; 4 | // @ts-expect-error process is a nodejs global 5 | const host = process.env.TAURI_DEV_HOST; 6 | 7 | // https://vitejs.dev/config/ 8 | export default defineConfig(async () => ({ 9 | plugins: [react(),svgr()], 10 | build: { 11 | target: 'esnext', // 将构建目标设置为 'esnext',以支持顶级 await 12 | }, 13 | // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` 14 | // 15 | // 1. prevent vite from obscuring rust errors 16 | clearScreen: false, 17 | // 2. tauri expects a fixed port, fail if that port is not available 18 | server: { 19 | port: 1420, 20 | strictPort: true, 21 | host: host || false, 22 | hmr: host 23 | ? { 24 | protocol: "ws", 25 | host, 26 | port: 1421, 27 | } 28 | : undefined, 29 | watch: { 30 | // 3. tell vite to ignore watching `src-tauri` 31 | ignored: ["**/src-tauri/**"], 32 | }, 33 | }, 34 | })); 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Tuyang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/assets/closed.svg: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1738671007165" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4347" xmlns:xlink="http://www.w3.org/1999/xlink" width="1em" height="1em"><path fill="currentcolor" d="M896 928a31.904 31.904 0 0 1-22.624-9.376l-192-192a31.968 31.968 0 1 1 45.248-45.248l192 192A31.968 31.968 0 0 1 896 928zM320 352a31.904 31.904 0 0 1-22.624-9.376l-192-192a31.904 31.904 0 0 1 0-45.248 31.968 31.968 0 0 1 45.248 0l192 192a31.904 31.904 0 0 1 0 45.248A31.904 31.904 0 0 1 320 352zM128 928a31.968 31.968 0 0 1-22.624-54.624l192-192a31.968 31.968 0 1 1 45.248 45.248l-192 192A31.904 31.904 0 0 1 128 928z m576-576a31.904 31.904 0 0 1-32-32c0-8.192 3.136-16.384 9.376-22.624l192-192a31.968 31.968 0 1 1 45.248 45.248l-192 192A31.904 31.904 0 0 1 704 352zM512 650.656A138.816 138.816 0 0 1 373.344 512 138.816 138.816 0 0 1 512 373.344 138.816 138.816 0 0 1 650.656 512 138.784 138.784 0 0 1 512 650.656z m0-213.312A74.72 74.72 0 0 0 437.344 512 74.752 74.752 0 0 0 512 586.656 74.752 74.752 0 0 0 586.656 512 74.752 74.752 0 0 0 512 437.344z" p-id="4348"></path></svg> -------------------------------------------------------------------------------- /src/mod/Deviation.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react'; 2 | import { Button, Flex, Slider, Tooltip } from 'antd'; 3 | import type { SliderSingleProps } from 'antd'; 4 | import { QuestionOutlined } from '@ant-design/icons'; 5 | const marks: SliderSingleProps['marks'] = { 6 | 0: '0', 7 | 15: '15', 8 | 30: '30', 9 | 45: '45', 10 | 59: "59" 11 | }; 12 | interface props { 13 | value: number 14 | setVal: (e: number) => void 15 | prompt: ReactNode 16 | } 17 | const App: React.FC<props> = ({ value, setVal, prompt }) => ( 18 | <Flex gap={8}> 19 | <Tooltip title={prompt} placement="bottom"> 20 | <Button 21 | size="small" 22 | type='text' 23 | icon={<QuestionOutlined />} 24 | variant="link" /> 25 | </Tooltip> 26 | <Slider 27 | style={{ 28 | width: 180, 29 | margin: '0 5px 14px 5px' 30 | }} 31 | marks={marks} 32 | included={false} 33 | value={value} 34 | onChange={setVal} 35 | max={60} 36 | min={0} 37 | defaultValue={20} /> 38 | 39 | </Flex> 40 | ); 41 | 42 | export default App; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "windows_theme_auto", 3 | "private": true, 4 | "version": "1.3.2", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview", 10 | "tauri": "tauri", 11 | "start": "tauri dev", 12 | "tauribuild": "tauri build" 13 | }, 14 | "dependencies": { 15 | "@ant-design/icons": "^6.1.0", 16 | "@ant-design/pro-components": "^2.8.10", 17 | "@tauri-apps/api": "^2", 18 | "@tauri-apps/plugin-autostart": "^2", 19 | "@tauri-apps/plugin-fs": "^2.4.4", 20 | "@tauri-apps/plugin-http": "^2", 21 | "@tauri-apps/plugin-log": "^2", 22 | "@tauri-apps/plugin-opener": "^2", 23 | "@tauri-apps/plugin-os": "^2", 24 | "@tauri-apps/plugin-window-state": "^2", 25 | "ahooks": "^3.9.5", 26 | "antd": "^5.28.1", 27 | "dayjs": "^1.11.18", 28 | "framer-motion": "^12.23.24", 29 | "motion": "^12.23.24", 30 | "pako": "^2.1.0", 31 | "react": "^18.3.1", 32 | "react-dom": "^18.3.1", 33 | "react-markdown": "^9.0.3", 34 | "zustand": "^5.0.8" 35 | }, 36 | "devDependencies": { 37 | "@tauri-apps/cli": "^2", 38 | "@types/pako": "^2.0.3", 39 | "@types/react": "^18.3.1", 40 | "@types/react-dom": "^18.3.1", 41 | "@vitejs/plugin-react": "^4.3.4", 42 | "typescript": "~5.6.2", 43 | "vite": "^6.0.3", 44 | "vite-plugin-svgr": "^4.3.0" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg> -------------------------------------------------------------------------------- /src-tauri/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "AutoThemeMode" 3 | version = "1.6.0" 4 | description = "AutoThemeMode" 5 | authors = ["you"] 6 | edition = "2021" 7 | 8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 9 | 10 | [lib] 11 | # The `_lib` suffix may seem redundant but it is necessary 12 | # to make the lib name unique and wouldn't conflict with the bin name. 13 | # This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519 14 | name = "tauri_app_lib" 15 | crate-type = ["staticlib", "cdylib", "rlib"] 16 | 17 | [build-dependencies] 18 | tauri-build = { version = "2", features = [] } 19 | windows-bindgen = "0.52.0" 20 | 21 | [dependencies] 22 | tauri-plugin-opener = "2" 23 | serde = { version = "1", features = ["derive"] } 24 | tauri = { version = "2", features = [ "tray-icon", "image-png" ] } 25 | serde_json = "1" 26 | tauri-plugin-http = "2" 27 | tokio = { version = "1", features = ["full", "rt-multi-thread", "macros"] } 28 | winapi = { version = "0.3", features = ["winuser", "winbase"] } 29 | chrono = "0.4" 30 | tauri-plugin-log = "2" 31 | tauri-plugin-persisted-scope = "2" 32 | tauri-plugin-os = "2.2.1" 33 | windows-applicationmodel = "0.23.0" 34 | windows = { version = "0.61.3", features = [] } 35 | tauri-plugin-fs = "2" 36 | ini = "1.3.0" 37 | configparser = "2.0" 38 | open = { version = "5.3.2", optional = true } 39 | winreg = "0.55.0" 40 | shellexpand = "2.1" # 新增 41 | dirs = "5.0" # 新增 42 | encoding_rs="0.8" 43 | 44 | [features] 45 | default = [] 46 | open-feature = ["dep:open"] 47 | 48 | [profile.release] 49 | codegen-units = 1 # 允许 LLVM 执行更好的优化。 50 | lto = true # 启用链接时优化。 51 | opt-level = "s" # 优先考虑小的二进制文件大小。如果您更喜欢速度,请使用 `3`。 52 | panic = "abort" # 通过禁用 panic 处理程序来提高性能。 53 | strip = true # 确保移除调试符号。 54 | 55 | [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] 56 | tauri-plugin-autostart = "2" 57 | tauri-plugin-single-instance = "2" 58 | tauri-plugin-window-state = "2" 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /src-tauri/tauri.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.tauri.app/config/2", 3 | "productName": "AutoThemeMode", 4 | "identifier": "com.AutoThemeMode.cn", 5 | "build": { 6 | "beforeDevCommand": "npm run dev", 7 | "devUrl": "http://localhost:1420", 8 | "beforeBuildCommand": "npm run build", 9 | "frontendDist": "../dist" 10 | }, 11 | "app": { 12 | "withGlobalTauri": true, 13 | "windows": [ 14 | { 15 | "title": "系统主题自适应", 16 | "label": "main", 17 | "windowEffects": { 18 | "color": "#00000000", 19 | "effects": [] 20 | }, 21 | "shadow": false, 22 | "decorations": false, 23 | "visible": false, 24 | "visibleOnAllWorkspaces": false, 25 | "focus": false, 26 | "alwaysOnTop": true, 27 | "transparent": true, 28 | "center": true, 29 | "maximizable": false, 30 | "dragDropEnabled": false, 31 | "maximized": false, 32 | "resizable": true, 33 | "maxHeight": 736, 34 | "maxWidth": 500, 35 | "minHeight": 572, 36 | "minWidth": 320, 37 | "width": 382, 38 | "height": 572 39 | } 40 | ], 41 | "security": { 42 | "csp": null 43 | } 44 | }, 45 | "plugins": { 46 | "autostart": null 47 | }, 48 | "bundle": { 49 | "active": true, 50 | "targets": [ 51 | "nsis" 52 | ], 53 | "copyright": "Copyright © 2025 Tuyang All", 54 | "homepage": "https://github.com/tuyangJs/Windows_AutoTheme", 55 | "windows": { 56 | "nsis": { 57 | "languages": [ 58 | "SimpChinese", 59 | "TradChinese", 60 | "English", 61 | "Spanish", 62 | "Japanese" 63 | ], 64 | "headerImage": "img/header.bmp", 65 | "installerIcon": "icons/installer.ico", 66 | "sidebarImage": "img/sidebar.bmp", 67 | "installMode": "both", 68 | "displayLanguageSelector": true 69 | } 70 | }, 71 | "icon": [ 72 | "icons/icon.png" 73 | ], 74 | "resources": [] 75 | } 76 | } -------------------------------------------------------------------------------- /src/language/zh-HK.json: -------------------------------------------------------------------------------- 1 | { 2 | "Title": "系統主題自動切換", 3 | "main": { 4 | "open": "開啟", 5 | "language": "多語言", 6 | "citiTitle": "城市", 7 | "citiError": "尚未選擇城市!", 8 | "citiPlaceholder": "輸入城市名搜尋", 9 | "switchStyemMode": "切換主題背景", 10 | "switchStyemModeTip": "系統主題變更時自動切換對應背景", 11 | "switchStyemModeOpenTip": "開啟後,應用主題時會顯示系統設定視窗", 12 | "winBgEffect": "視窗背景材質", 13 | "Mica": "雲母", 14 | "Acrylic": "壓克力", 15 | "Default": "預設", 16 | "TabsOptionA": "日出到日落", 17 | "TabsOptionAError": "取得日出日落資訊失敗!", 18 | "TabsOptionB": "白天時間", 19 | "Autostart": "跟隨系統啟動", 20 | "AutostartTip": "需手動找到 Auto Theme Mode 設定,點擊按鈕前往設定", 21 | "AutostartBtn": "前往設定", 22 | "StartShow": "啟動時顯示視窗", 23 | "deviationTitle": "時間偏差", 24 | "deviationPrompt": "調整白天時間偏移量(單位:分鐘)日出延後、日落提前" 25 | }, 26 | "upModal": { 27 | "textA": [ 28 | "檢查中:", 29 | "發現新版本", 30 | "已是最新版本" 31 | ], 32 | "textB": [ 33 | "更新中...", 34 | "查看更新", 35 | "檢查更新" 36 | ], 37 | "title": "程式有新版本:", 38 | "cancelText": "取消", 39 | "okText": "前往更新", 40 | "upData": "更新日誌", 41 | "noText": "忽略本次更新" 42 | }, 43 | "reviewModal": { 44 | "title": "喜歡這個應用嗎?", 45 | "text": "您的評分對我們很重要!是否願意在 Microsoft Store 為我們評分?", 46 | "laterText": "稍後再說", 47 | "cancelText": "不再提醒", 48 | "okText": "前往評分" 49 | }, 50 | "quit": "結束程式", 51 | "show": "顯示視窗", 52 | "Time": "時間", 53 | "dark": "深色", 54 | "light": "淺色", 55 | "switch": "切換至", 56 | "ThemeDark": "深色主題", 57 | "ThemeLight": "淺色主題", 58 | "doc": [ 59 | "天氣", 60 | "說明", 61 | "本程式由:荼泱Tuyang 個人開發,完全開源免費", 62 | "取得協助:" 63 | ], 64 | "themeName": { 65 | "spotlight": "Windows 聚焦,動態影像", 66 | "dark": "Windows 深色", 67 | "themeA": "發光", 68 | "themeB": "動感捕捉", 69 | "themeD": "流暢", 70 | "themeC": "日出", 71 | "Custom": "自訂" 72 | } 73 | } -------------------------------------------------------------------------------- /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 | "opener:default", 11 | "core:window:default", 12 | "core:window:allow-start-dragging", 13 | "core:window:allow-hide", 14 | "core:window:allow-show", 15 | "core:window:allow-close", 16 | "core:window:allow-destroy", 17 | "core:window:allow-minimize", 18 | "core:webview:allow-webview-hide", 19 | "core:webview:allow-webview-show", 20 | "core:window:allow-set-decorations", 21 | "core:window:allow-set-title", 22 | "core:window:allow-set-effects", 23 | "autostart:allow-enable", 24 | "autostart:allow-disable", 25 | "autostart:allow-is-enabled", 26 | "window-state:allow-save-window-state", 27 | "log:default", 28 | "fs:allow-stat", 29 | "fs:allow-read", 30 | "fs:read-all", 31 | "fs:write-all", 32 | "fs:allow-create", 33 | { 34 | "identifier": "fs:default", 35 | "allow": [ 36 | { 37 | "path": "**" 38 | }, 39 | "fs:allow-write-recursive", 40 | "fs:scope-fs-recursive" 41 | ] 42 | }, 43 | { 44 | "identifier": "opener:allow-open-url", 45 | "allow": [ 46 | { 47 | "url": "ms-windows-store://*" 48 | }, 49 | { 50 | "url": "ms-settings:*" 51 | }, 52 | { 53 | "url": "https://apps.microsoft.com/*" 54 | } 55 | ] 56 | }, 57 | { 58 | "identifier": "http:default", 59 | "allow": [ 60 | { 61 | "url": "https://*.qweather.com" 62 | }, 63 | { 64 | "url": "https://api.sunrise-sunset.org" 65 | }, 66 | { 67 | "url": "http://demo.ip-api.com/**" 68 | } 69 | ], 70 | "deny": [ 71 | { 72 | "url": "https://api.qweather.com" 73 | }, 74 | { 75 | "url": "https://*.ip-api.com" 76 | } 77 | ] 78 | } 79 | ] 80 | } -------------------------------------------------------------------------------- /src/language/zh-CN.json: -------------------------------------------------------------------------------- 1 | { 2 | "Title": "系统主题自适应", 3 | "main": { 4 | "open": "开启", 5 | "language": "多语言", 6 | "citiTitle": "城市", 7 | "citiError": "你还没有选择城市!", 8 | "citiPlaceholder": "输入城市名搜索", 9 | "switchStyemMode": "切换主题背景", 10 | "switchStyemModeTip": "切换系统主题时,自动切换为对应的主题背景。", 11 | "switchStyemModeOpenTip": "开启后,应用主题时会打开系统设置窗口。", 12 | "winBgEffect": "窗口背景材料", 13 | "Mica": "云母", 14 | "Acrylic": "亚克力", 15 | "Default": "默认", 16 | "TabsOptionA": "日出到日落", 17 | "TabsOptionAError": "获取日出日落信息失败!", 18 | "TabsOptionB": "白天时间", 19 | "Autostart": "跟随系统启动", 20 | "AutostartTip": "修改本选项需手动找到 Auto Theme Mode,点击按钮前往设置", 21 | "AutostartBtn": "前往设置", 22 | "StartShow": "启动时显示窗口", 23 | "deviationTitle": "时间偏差", 24 | "deviationPrompt": "根据数值偏移白天时间(单位:分钟)日出时间增加、日落时间提早。" 25 | }, 26 | "upModal": { 27 | "textA": [ 28 | "正在检查:", 29 | "发现新版本", 30 | "已是最新版" 31 | ], 32 | "textB": [ 33 | "更新......", 34 | "查看更新", 35 | "检查更新" 36 | ], 37 | "title": "程序有新版本:", 38 | "cancelText": "取消", 39 | "okText": "前往更新", 40 | "upData": "更新日志", 41 | "noText": "忽略本次更新" 42 | }, 43 | "reviewModal": { 44 | "title": "喜欢这个应用吗?", 45 | "text": "您的评分对我们非常重要!是否愿意花一点时间在 Microsoft Store 上给我们评分?", 46 | "laterText": "稍后再说", 47 | "cancelText": "不再提醒", 48 | "okText": "去评分" 49 | }, 50 | "quit": "退出程序", 51 | "show": "显示窗口", 52 | "Time": "时间", 53 | "dark": "暗黑", 54 | "light": "浅色", 55 | "switch": "切换到", 56 | "ThemeDark": "深色主题", 57 | "ThemeLight": "浅色主题", 58 | "doc": [ 59 | "天气", 60 | "说明", 61 | " 本程序由:荼泱Tuyang 个人开发完全开源免费。", 62 | "获取帮助:" 63 | ], 64 | "themeName": { 65 | "spotlight": "Windows聚焦,动态图像", 66 | "dark": "Windows深色", 67 | "themeA": "发光", 68 | "themeB": "捕获的动作", 69 | "themeD": "流畅", 70 | "themeC": "日出", 71 | "Custom": "自定义" 72 | } 73 | } -------------------------------------------------------------------------------- /src/assets/min.svg: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1738671109686" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="15412" xmlns:xlink="http://www.w3.org/1999/xlink" width="1em" height="1em"><path d="M319.27 641.9H190.79c-34.64 0-62.83 28.18-62.83 62.83v128.48c0 34.64 28.18 62.83 62.83 62.83h128.48c34.64 0 62.83-28.18 62.83-62.83V704.73c0-34.65-28.18-62.83-62.83-62.83z m22.83 191.31c0 12.59-10.24 22.83-22.83 22.83H190.79c-12.59 0-22.83-10.24-22.83-22.83V704.73c0-12.59 10.24-22.83 22.83-22.83h128.48c12.59 0 22.83 10.24 22.83 22.83v128.48z" fill="currentColor" p-id="15413"></path><path d="M790.38 127.96H276.45c-58.26 0-105.66 47.4-105.66 105.66v256.97c0 11.05 8.95 20 20 20s20-8.95 20-20V233.62c0-36.2 29.45-65.66 65.66-65.66h513.94c36.2 0 65.66 29.45 65.66 65.66v513.94c0 36.2-29.45 65.66-65.66 65.66H533.41c-11.05 0-20 8.95-20 20s8.95 20 20 20h256.97c58.26 0 105.66-47.4 105.66-105.66V233.62c0-58.26-47.4-105.66-105.66-105.66z" fill="currentColor 2 | " p-id="15414"></path><path d="M478.64 549.43c0.28 0.21 0.55 0.42 0.84 0.62 0.29 0.19 0.59 0.36 0.89 0.54 0.26 0.16 0.52 0.32 0.79 0.47 0.3 0.16 0.61 0.3 0.92 0.44 0.28 0.13 0.56 0.27 0.85 0.39 0.3 0.12 0.61 0.22 0.91 0.33 0.31 0.11 0.62 0.23 0.94 0.33 0.3 0.09 0.61 0.16 0.92 0.24 0.33 0.08 0.65 0.17 0.98 0.24 0.35 0.07 0.71 0.11 1.06 0.16 0.29 0.04 0.58 0.1 0.87 0.13 0.66 0.06 1.31 0.1 1.97 0.1H661.9c11.05 0 20-8.95 20-20s-8.95-20-20-20H538.87l180-180c7.81-7.81 7.81-20.47 0-28.28-7.81-7.81-20.47-7.81-28.29 0l-180 180V362.1c0-11.05-8.95-20-20-20s-20 8.95-20 20v171.31c0 0.66 0.03 1.32 0.1 1.97 0.03 0.3 0.09 0.59 0.13 0.88 0.05 0.35 0.09 0.7 0.16 1.05 0.07 0.34 0.16 0.66 0.24 0.99 0.08 0.3 0.14 0.61 0.23 0.91 0.1 0.32 0.22 0.63 0.33 0.95 0.11 0.3 0.21 0.6 0.33 0.9 0.12 0.29 0.27 0.58 0.4 0.86 0.14 0.31 0.28 0.61 0.44 0.91 0.15 0.27 0.31 0.53 0.47 0.8 0.18 0.29 0.34 0.59 0.53 0.88 0.2 0.29 0.41 0.57 0.62 0.85 0.18 0.24 0.35 0.49 0.54 0.72 0.41 0.49 0.84 0.97 1.29 1.42 0.01 0.01 0.02 0.03 0.04 0.04l0.05 0.05c0.45 0.45 0.92 0.87 1.41 1.28 0.26 0.21 0.51 0.38 0.75 0.56z" fill="currentColor" p-id="15415"></path></svg> -------------------------------------------------------------------------------- /src/language/ja-JP.json: -------------------------------------------------------------------------------- 1 | { 2 | "Title": "システムテーマ自動切替", 3 | "main": { 4 | "open": "有効", 5 | "language": "多言語", 6 | "citiTitle": "都市", 7 | "citiError": "都市が選択されていません!", 8 | "citiPlaceholder": "都市名で検索", 9 | "switchStyemMode": "テーマ背景切替", 10 | "switchStyemModeTip": "システムテーマ変更時、対応する背景に自動切替", 11 | "switchStyemModeOpenTip": "有効時、テーマ適用でシステム設定画面を表示", 12 | "winBgEffect": "ウィンドウ背景素材", 13 | "Mica": "マイカ", 14 | "Acrylic": "アクリル", 15 | "Default": "デフォルト", 16 | "TabsOptionA": "日出から日没まで", 17 | "TabsOptionAError": "日出日落情報取得失敗!", 18 | "TabsOptionB": "日中時間", 19 | "Autostart": "システム起動時実行", 20 | "AutostartTip": "Auto Theme Modeを手動で設定、クリックで設定画面へ", 21 | "AutostartBtn": "設定へ", 22 | "StartShow": "起動時ウィンドウ表示", 23 | "deviationTitle": "時間偏差", 24 | "deviationPrompt": "日中時間オフセット調整(分単位)日出遅延、日落提前" 25 | }, 26 | "upModal": { 27 | "textA": [ 28 | "チェック中:", 29 | "新バージョン発見", 30 | "最新版です" 31 | ], 32 | "textB": [ 33 | "更新中...", 34 | "更新確認", 35 | "更新チェック" 36 | ], 37 | "title": "プログラム新バージョン:", 38 | "cancelText": "キャンセル", 39 | "okText": "更新へ", 40 | "upData": "更新履歴", 41 | "noText": "今回の更新を無視" 42 | }, 43 | "reviewModal": { 44 | "title": "このアプリはお好きですか?", 45 | "text": "評価は重要です!Microsoft Storeで評価をお願いします", 46 | "laterText": "後で", 47 | "cancelText": "通知停止", 48 | "okText": "評価する" 49 | }, 50 | "quit": "プログラム終了", 51 | "show": "ウィンドウ表示", 52 | "Time": "時間", 53 | "dark": "ダーク", 54 | "light": "ライト", 55 | "switch": "切替", 56 | "ThemeDark": "ダークテーマ", 57 | "ThemeLight": "ライトテーマ", 58 | "doc": [ 59 | "天気", 60 | "説明", 61 | "本プログラム:荼泱Tuyang 個人開発、完全オープンソース無料", 62 | "ヘルプ取得:" 63 | ], 64 | "themeName": { 65 | "spotlight": "Windowsスポットライト、動画", 66 | "dark": "Windowsダーク", 67 | "themeA": "発光", 68 | "themeB": "モーションキャプチャ", 69 | "themeD": "フルーエント", 70 | "themeC": "日出", 71 | "Custom": "カスタム" 72 | } 73 | } -------------------------------------------------------------------------------- /src/mod/Crontab.ts: -------------------------------------------------------------------------------- 1 | // 定义任务类型 2 | export interface CrontabTask { 3 | time: string; // 任务时间(HH:mm) 4 | data: any; // 任务数据 5 | onExecute: (time: string, data: any) => void; // 任务执行回调 6 | } 7 | 8 | // 任务管理器 9 | export class CrontabManager { 10 | private static tasks: Map<string, { timeout: number; interval: number }> = new Map(); 11 | 12 | /** 13 | * 添加或覆盖定时任务 14 | * @param task {CrontabTask} - 任务对象 15 | */ 16 | static addTask(task: CrontabTask): void { 17 | const { time, data, onExecute } = task; 18 | const [hours, minutes] = time.split(":").map(Number); 19 | 20 | const now = new Date(); 21 | const targetTime = new Date(); 22 | targetTime.setHours(hours, minutes, 0, 0); 23 | 24 | let delay = targetTime.getTime() - now.getTime(); 25 | if (delay < 0) { 26 | // 目标时间已过,延迟到明天 27 | delay += 24 * 60 * 60 * 1000; 28 | } 29 | 30 | // 如果已存在相同时间的任务,先清除 31 | CrontabManager.removeTask(time); 32 | 33 | // 设置定时任务 34 | const timeout = window.setTimeout(() => { 35 | // 执行任务回调 36 | onExecute(time, data); 37 | 38 | // 每天相同时间运行 39 | const interval = window.setInterval(() => { 40 | onExecute(time, data); 41 | }, 24 * 60 * 60 * 1000); 42 | // 更新任务信息 43 | CrontabManager.tasks.set(time, { timeout, interval }); 44 | }, delay); 45 | 46 | // 初始任务信息存储 47 | CrontabManager.tasks.set(time, { timeout, interval: 0 }); 48 | } 49 | 50 | /** 51 | * 删除指定时间的定时任务 52 | * @param time {string} - 任务时间 (格式 'HH:mm') 53 | */ 54 | static removeTask(time: string): void { 55 | if (CrontabManager.tasks.has(time)) { 56 | const task = CrontabManager.tasks.get(time)!; 57 | clearTimeout(task.timeout); 58 | clearInterval(task.interval); 59 | CrontabManager.tasks.delete(time); 60 | console.log(`已删除定时任务: ${time}`); 61 | } 62 | } 63 | 64 | /** 65 | * 清除所有任务 66 | */ 67 | static clearAllTasks(): void { 68 | CrontabManager.tasks.forEach(({ timeout, interval }) => { 69 | clearTimeout(timeout); 70 | clearInterval(interval); 71 | }); 72 | CrontabManager.tasks.clear(); 73 | console.log("所有定时任务已清除"); 74 | } 75 | 76 | /** 77 | * 获取所有任务 78 | * @returns {string[]} - 当前已设置的所有任务时间 79 | */ 80 | static listTasks(): string[] { 81 | return Array.from(CrontabManager.tasks.keys()); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/doc.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button, Divider, Tooltip, Typography } from 'antd'; 3 | import { InfoCircleOutlined } from '@ant-design/icons'; 4 | import StoreLogo from "./assets/StoreLogo.svg?react"; 5 | import { openStoreRating } from './mod/openStoreRating'; 6 | const { Paragraph, Link, Text } = Typography; 7 | 8 | interface Props { 9 | locale: any 10 | version: string 11 | Weather?: string 12 | isWin64App: boolean 13 | } 14 | const App: React.FC<Props> = ({ locale, version, Weather, isWin64App }) => ( 15 | <Typography> 16 | {Weather ? ( 17 | <> 18 | <Divider style={{ marginBlock: 6 }}> 19 | <Text type="secondary"> {locale?.doc?.[0]}</Text> 20 | </Divider><Paragraph 21 | type="secondary" 22 | ellipsis={{ rows: 2, expandable: true }} 23 | > 24 | {Weather} 25 | </Paragraph> 26 | </> 27 | ) : null} 28 | <Divider style={{ margin: 0 }}><Text type="secondary"> v{version}</Text> </Divider> 29 | <Paragraph type="secondary"> 30 | <Tooltip title={locale?.doc?.[2]}> 31 | <Button 32 | type='text' 33 | icon={<InfoCircleOutlined />} 34 | variant="link" /> 35 | </Tooltip> 36 | {locale?.doc?.[3]} 37 | { 38 | isWin64App && 39 | <><Button 40 | type='text' 41 | icon={<StoreLogo />} 42 | variant="link" 43 | onClick={() => openStoreRating()} 44 | /> 45 | <Divider style={{ marginInline: 6 }} type='vertical' /> 46 | </> 47 | } 48 | <Link 49 | target='_blank' 50 | href='https://github.com/tuyangJs/Windows_AutoTheme' 51 | >{' '} 52 | GitHub 53 | </Link> 54 | <Divider style={{ marginInline: 6 }} type='vertical' /> 55 | <Link 56 | target='_blank' 57 | href='https://gitee.com/ilinxuan/windows_-auto-theme' 58 | >{' '} 59 | Gitee 60 | </Link> 61 | 62 | </Paragraph> 63 | </Typography> 64 | ); 65 | 66 | export default App; -------------------------------------------------------------------------------- /src/mod/utils/path.ts: -------------------------------------------------------------------------------- 1 | // utils/path.ts 2 | export function normalizeWindowsPath(path?: string): string { 3 | if (!path) return ''; 4 | 5 | // 保持原始字符串不被意外修改 6 | let p = path; 7 | 8 | // 处理 \\?\UNC\server\share\... -> \\server\share\... 9 | const prefixUNC = '\\\\?\\UNC\\'; 10 | if (p.startsWith(prefixUNC)) { 11 | const rest = p.slice(prefixUNC.length); // server\share\... 12 | return '\\\\' + rest; 13 | } 14 | 15 | // 处理 \\?\C:\... -> C:\... 16 | const prefixExtended = '\\\\?\\'; 17 | if (p.startsWith(prefixExtended)) { 18 | return p.slice(prefixExtended.length); 19 | } 20 | 21 | // 处理 ?\C:\... -> C:\... 22 | const prefixQuestion = '?\\'; 23 | if (p.startsWith(prefixQuestion)) { 24 | return p.slice(prefixQuestion.length); 25 | } 26 | 27 | // 如果已经是正常路径,直接返回 28 | return p; 29 | } 30 | 31 | /** 32 | * 把 Windows 路径转换为 file:// URL,适用于 <img src="...">。 33 | * - 本地驱动器路径: C:\a\b -> file:///C:/a/b 34 | * - UNC 路径: \\server\share\a\b -> file://server/share/a/b 35 | */ 36 | export function windowsPathToFileUrl(path?: string): string { 37 | const norm = normalizeWindowsPath(path); 38 | if (!norm) return ''; 39 | 40 | // 如果是 UNC 路径(以两个反斜杠开头) 41 | if (norm.startsWith('\\\\')) { 42 | // 移除开头的两个反斜杠,再把反斜杠转为斜杠 43 | const withoutSlashes = norm.replace(/^\\\\+/, ''); 44 | const posix = withoutSlashes.replace(/\\/g, '/'); 45 | // 使用 file://server/share/... 46 | return 'file://' + encodeURI(posix); 47 | } 48 | 49 | // 驱动器路径 C:\... 50 | // 转为 file:///C:/... 51 | const posix = norm.replace(/\\/g, '/'); 52 | // encodeURI 保留斜杠但对空格及非 ASCII 做编码 53 | return 'file:///' + encodeURI(posix); 54 | } 55 | 56 | /** 57 | * 便利函数:接受 Theme 对象并返回拷贝,且规范化 wallpaper / displayWallpaper 字段 58 | */ 59 | export interface Theme { 60 | name: string; 61 | path: string; 62 | is_active: boolean; 63 | wallpaper?: string; 64 | system_mode?: string; 65 | app_mode?: string; 66 | displayPath?: string; 67 | displayWallpaper?: string; 68 | } 69 | 70 | export function normalizeThemePaths(theme: Theme): Theme { 71 | return { 72 | ...theme, 73 | wallpaper: normalizeWindowsPath(theme.wallpaper), 74 | displayWallpaper: normalizeWindowsPath(theme.displayWallpaper), 75 | // 你也可以同时规范 displayPath 或 path,如果需要就解除注释: 76 | // displayPath: normalizeWindowsPath(theme.displayPath), 77 | // path: normalizeWindowsPath(theme.path), 78 | }; 79 | } 80 | -------------------------------------------------------------------------------- /src/mod/searchCiti.tsx: -------------------------------------------------------------------------------- 1 | import { AppCiti } from "./sociti"; 2 | import { AppDataType } from "../Type"; 3 | //渲染搜索结果 4 | const searchResult = async (query: string, AppData: AppDataType | undefined) => { 5 | if (!AppData?.language) return []; 6 | const data = await AppCiti(query, AppData.language) 7 | if (data.code !== '200') return []; 8 | const CityList = data?.location || data?.topCityList 9 | console.log(data); 10 | 11 | return CityList 12 | .map((e: any) => { 13 | return { 14 | value: formatCityDisplayByHierarchy(e), 15 | key: e.id, 16 | position: { lat: e.lat, lng: e.lon, tzid: e.tz }, 17 | label: ( 18 | <div 19 | key={e.id} 20 | style={{ 21 | display: 'flex', 22 | justifyContent: 'space-between', 23 | }} 24 | > 25 | <span> 26 | {e.country} 27 | <a 28 | > 29 | {formatCityDisplayByHierarchy(e)} 30 | </a> 31 | </span> 32 | 33 | </div> 34 | ), 35 | }; 36 | }); 37 | } 38 | 39 | export { searchResult } 40 | export const formatCityDisplayByHierarchy = (cityData: any): string => { 41 | const { adm1, adm2, name } = cityData; 42 | 43 | // 推断行政层级关系 44 | const inferHierarchy = () => { 45 | // 如果 adm1 和 adm2 完全相同,可能是直辖市或特殊城市 46 | if (adm1 === adm2) { 47 | return 'same_level'; 48 | } 49 | 50 | // 如果 adm2 和 name 相同,可能是城市与其下辖区同名 51 | if (adm2 === name) { 52 | return 'city_district_same'; 53 | } 54 | 55 | // 如果三个字段都不同,则是完整的省-市-区层级 56 | if (adm1 !== adm2 && adm2 !== name && adm1 !== name) { 57 | return 'full_hierarchy'; 58 | } 59 | 60 | return 'unknown'; 61 | }; 62 | 63 | const hierarchy = inferHierarchy(); 64 | 65 | switch (hierarchy) { 66 | case 'same_level': 67 | // adm1 和 adm2 相同:显示为 "adm1 - name" 68 | return `${adm1} - ${name}`; 69 | 70 | case 'city_district_same': 71 | // adm2 和 name 相同:显示为 "adm1 - adm2" 72 | return `${adm1} - ${adm2}`; 73 | 74 | case 'full_hierarchy': 75 | // 完整的三个层级:显示为 "adm1 - adm2 - name" 76 | return `${adm1} - ${adm2} - ${name}`; 77 | 78 | default: 79 | // 未知情况:去重后连接 80 | const parts = [adm1, adm2, name].filter( 81 | (part, index, array) => part && part !== array[index - 1] 82 | ); 83 | return parts.join(' - '); 84 | } 85 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Windows AutoTheme 2 | <div align="center"> 3 | <img src="https://github.com/user-attachments/assets/c3cdbcf6-6bdc-4e91-a84a-55ef109c60f5" alt="Screenshot 1" width="100%"> 4 | 5 | #### Language: [中文](/README.md) | [English](/README-English.md) 6 | </div> 7 | 8 | > 现已登陆 **Microsoft Store**!获取更安全的安装体验和自动更新 9 | 10 | 11 | ## 概述 12 | 13 | **Windows AutoTheme** 是一个轻量级的 Windows 主题自动切换工具,支持系统在日间使用浅色模式,夜间切换为深色模式。<br /> 14 | 该项目利用 Rust 构建后端代码来执行系统操作,并采用 TypeScript 与 Ant Design 5 构建前端界面。<br />同时,它通过内置免费 API 获取日出和日落数据,实现智能自动切换主题模式。 15 | <div align="center"> 16 | <img src="https://github.com/user-attachments/assets/8ed6411d-cc19-4884-a2b6-8d0d65f64078" alt="Screenshot 2" width="55%"> 17 | </div> 18 | 19 | --- 20 | 21 | 22 | 23 | 24 | ## 🛠️ 安装方式 25 | ### 推荐安装(Microsoft Store): 26 | [![获取应用](https://get.microsoft.com/images/zh-cn%20light.svg)](https://apps.microsoft.com/detail/9n7nd584tdv1) 27 | 28 | 29 | ### 传统安装: 30 | >打开我们的[发行页面](https://github.com/tuyangJs/Windows_AutoTheme/releases),下载最新版本的安装包。 31 | 32 | 33 | ## 功能特点 34 | 35 | - **自动切换**:根据日出、日落时间自动切换 Windows 主题模式。 36 | - **高效轻量**:使用 Rust 提供高效的系统调用,保证运行稳定。 37 | - **现代前端**:前端采用 TypeScript 与 Ant Design 5 定制界面,简洁美观。 38 | - **免费天文数据支持**:集成免费 API,实时获取日出和日落时间。 39 | 40 | --- 41 | 42 | ## 截图 43 | 44 | <div align="center"> 45 | <img src="https://github.com/user-attachments/assets/5f0c5730-a398-482c-8e6c-e49067d2fe24" alt="pshotA.png" width="45%" style="margin-right: 5%;"> 46 | </div> 47 | 48 | --- 49 | 50 | ## 打赏 51 | 52 | <img src="https://github.com/user-attachments/assets/1be236a6-504a-4beb-b76c-ba6192730ef3" alt="支付宝.png" width="45%" style="margin-right: 5%;"> 53 | <img src="https://github.com/user-attachments/assets/5e6f50f5-6712-429a-8c7f-d6ceb87a6cd6" alt="微信.png" width="45%" style="margin-right: 5%;"> 54 | 55 | 56 | 57 | ## 开发调试 58 | 安装依赖 59 | ``` 60 | npm install 61 | ``` 62 | 调试 63 | ``` 64 | npm start 65 | ``` 66 | 编译 67 | ``` 68 | npm run tauri build 69 | ``` 70 | ## 其它仓库 71 | - Gitee (国内推荐): [https://gitee.com/ilinxuan/windows_-auto-theme](https://gitee.com/ilinxuan/windows_-auto-theme) 72 | - GitHub: [https://github.com/tuyangJs/Windows_AutoTheme](https://github.com/tuyangJs/Windows_AutoTheme) 73 | 74 | ## 联系作者 75 | 76 | - QQ群 : [703623743](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=IVNKPTJ9WqoIHHCsy7UMkQd16NLnfjeD&authKey=WVTDqfUgdv9oV0d8%2BZz5krS98IIlB1Kuvm%2BS3pfMU1H6FBCV1b2xoG5pWsggiAgt&noverify=0&group_code=703623743) 77 | - Email : [ihanlong@qq.com](ihanlong@qq.com) 78 | -------------------------------------------------------------------------------- /src/mod/WindowCode.ts: -------------------------------------------------------------------------------- 1 | import { getCurrentWebview } from "@tauri-apps/api/webview"; 2 | import { useEffect } from "react"; 3 | import { Window, Effect } from '@tauri-apps/api/window'; // 引入 appWindow 4 | import { AppDataType } from "../Type"; 5 | import { listen } from "@tauri-apps/api/event"; 6 | import { saveWindowState, StateFlags } from "@tauri-apps/plugin-window-state"; 7 | 8 | const appWindow = new Window('main'); 9 | const Webview = await getCurrentWebview() 10 | window.appWindow = appWindow 11 | window.Webview = Webview 12 | //隐藏窗口 13 | appWindow.onCloseRequested(e => { 14 | e.preventDefault() 15 | setTimeout(() => { 16 | appWindow.hide() 17 | Webview.hide() 18 | saveWindowState(StateFlags.ALL) 19 | }, 22); 20 | }) 21 | 22 | 23 | listen("show-app", async () => { 24 | console.log("显示程序"); 25 | Webview.show() 26 | }); 27 | listen("close-app", async () => { 28 | console.log("收到后端关闭指令,正在退出应用..."); 29 | appWindow.hide() 30 | Webview.hide() 31 | saveWindowState(StateFlags.ALL) 32 | await appWindow.destroy(); 33 | }); 34 | export const WindowBg = (AppData: AppDataType, themeDack: boolean) => { 35 | if (AppData?.winBgEffect) { 36 | const types = AppData.winBgEffect === 'Acrylic' ? Effect.Acrylic : (themeDack ? Effect.Mica : Effect.Tabbed) 37 | appWindow.setEffects({ effects: [types] }) 38 | } 39 | } 40 | export const MainWindow = (setMainShow: (e: boolean) => void, AppData: AppDataType) => { 41 | useEffect(() => { 42 | (async () => { 43 | if (AppData?.StartShow) { 44 | appWindow.show() 45 | setMainShow(true) 46 | } else { 47 | if (await appWindow.isVisible()) { 48 | Webview.show() 49 | }else{ 50 | Webview.hide() 51 | } 52 | } 53 | })() 54 | 55 | const visibilitychange = () => { 56 | if (document.visibilityState === 'visible') { 57 | console.log('页面变得可见'); 58 | setMainShow(true) 59 | // 页面变得可见时执行的代码 60 | } else { 61 | console.log('页面变得不可见'); 62 | setMainShow(false) 63 | } 64 | } 65 | //监听窗口是否 66 | appWindow.onFocusChanged(async () => { 67 | if (await appWindow.isVisible()) { 68 | Webview.show() 69 | } 70 | }); 71 | //监听页面是否可视 72 | document.addEventListener('visibilitychange', visibilitychange); 73 | 74 | return () => { 75 | document.removeEventListener('visibilitychange', visibilitychange); 76 | } 77 | }, []) 78 | } 79 | -------------------------------------------------------------------------------- /src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <!-- Generator: Adobe Illustrator 28.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> 3 | <svg version="1.1" id="_图层_2" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" 4 | viewBox="0 0 685 685" style="enable-background:new 0 0 685 685;" xml:space="preserve"> 5 | <style type="text/css"> 6 | .st0{fill:#282933;} 7 | .st1{fill:#F0960E;} 8 | .st2{fill:#FDB10A;} 9 | .st3{fill:#F8A500;} 10 | .st4{fill:#FDB20A;} 11 | </style> 12 | <path class="st0" d="M175.4,0h334.2C606.5,0,685,81.8,685,182.8v319.4c0,101-78.5,182.8-175.4,182.8H175.4C78.5,685,0,603.2,0,502.2 13 | V182.8C0,81.8,78.5,0,175.4,0z"/> 14 | <path class="st1" d="M342.5,90.5c-1.8,0-3.6,0-5.4,0c-9,0.2-12.3,12.1-4.5,16.6c52,30.3,85.8,88.3,81.2,153.8 15 | c-5.6,81.1-71.5,146.8-152.6,152.3c-65.8,4.5-123.9-29.8-154-82.2c-4.5-7.8-16.4-4.5-16.6,4.5c-0.1,2.3-0.1,4.7-0.1,7 16 | c0,143.1,119.3,258.4,263.9,251.8c129.1-5.9,233.9-110.8,239.9-239.9C600.9,209.9,485.6,90.5,342.5,90.5L342.5,90.5L342.5,90.5z"/> 17 | <path class="st2" d="M191.7,306.7c0,14,7.5,27,19.7,34.1c12.2,7,27.2,7,39.3,0c12.2-7,19.7-20,19.7-34.1s-7.5-27-19.7-34.1 18 | c-12.2-7-27.2-7-39.3,0C199.2,279.6,191.7,292.6,191.7,306.7L191.7,306.7z"/> 19 | <path class="st3" d="M342.5,90.5c-1.8,0-3.6,0-5.4,0c-9,0.2-12.3,12.1-4.5,16.6c52,30.3,85.8,88.3,81.2,153.8 20 | c-5.6,81.1-71.5,146.8-152.6,152.3c-65.8,4.5-123.9-29.8-154-82.2c-4.5-7.8-16.4-4.5-16.6,4.5c-0.1,2.3-0.1,4.7-0.1,7 21 | c0,84.1,41.3,158.6,104.6,204.4c27.4,8.5,56,12.7,84.7,12.7c158.9,0,287.8-128.9,287.8-287.8c0-17.6-1.6-34.7-4.7-51.4 22 | C519.9,143,437.4,90.5,342.5,90.5L342.5,90.5L342.5,90.5z"/> 23 | <path class="st4" d="M337,90.6c-9,0.2-12.3,12.1-4.5,16.6c52,30.3,85.8,88.3,81.2,153.8c-5.6,81.1-71.5,146.8-152.6,152.3 24 | c-65.8,4.5-123.9-29.8-154-82.2c-4.5-7.8-16.4-4.5-16.6,4.5c-0.1,2.3-0.1,4.6-0.1,7c0,39.3,9.1,76.5,25.1,109.7 25 | c28.3,9,57.7,13.6,87.4,13.5c158.9,0,287.8-128.9,287.8-287.8c0-14-1-27.8-3-41.3c-41.1-29-91.2-46.1-145.3-46.1 26 | C340.7,90.5,338.9,90.5,337,90.6L337,90.6L337,90.6L337,90.6z"/> 27 | <path class="st2" d="M107.2,331.1c-4.5-7.8-16.4-4.5-16.6,4.5c-0.1,2.3-0.1,4.6-0.1,7c0,8.3,0.4,16.4,1.2,24.5 28 | c12.7,1.7,25.7,2.7,38.9,2.7c2.4,0,4.9-0.1,7.3-0.2C125.8,358.4,115.4,345.4,107.2,331.1L107.2,331.1L107.2,331.1L107.2,331.1 29 | L107.2,331.1z M342.5,90.5c-1.8,0-3.6,0-5.4,0c-9,0.2-12.3,12.1-4.5,16.6c29.9,17.5,53.5,43.9,67.6,75.5 30 | c9.7-25.9,15.5-53.2,17.4-80.8C393.3,94.3,367.9,90.5,342.5,90.5L342.5,90.5L342.5,90.5z"/> 31 | </svg> 32 | -------------------------------------------------------------------------------- /src/Content.tsx: -------------------------------------------------------------------------------- 1 | import { Divider, Flex, Switch, Typography } from "antd"; 2 | import type { mainsType } from "./mod/Mainoption"; 3 | import React from "react"; 4 | import { AnimatePresence, motion } from "framer-motion"; 5 | 6 | export interface props { 7 | mains: mainsType[]; 8 | language: string 9 | } 10 | const { Text } = Typography; 11 | 12 | const Content: React.FC<props> = ({ mains, language }) => ( 13 | <AnimatePresence mode="popLayout" key="contents"> 14 | {mains.map((item, i) => { 15 | // 只渲染不隐藏的选项 16 | if (item.hide) return null; 17 | 18 | // 确保每个item都有唯一key,优先使用item.key,否则使用索引和随机数组合 19 | const uniqueKey = item.key ? `${item.key}-${language}` : `${i}-${language}-${Math.random()}`; 20 | return ( 21 | <motion.div 22 | key={uniqueKey} 23 | layout 24 | initial={{ opacity: 0, x: 0, scale: 3, filter: "blur(5px)" }} 25 | animate={{ opacity: 1, x: 0, scale: 1, filter: "blur(0px)" }} 26 | exit={{ 27 | opacity: 0, 28 | x: 100, 29 | filter: "blur(5px)", 30 | transition: { 31 | duration: 0.28, 32 | delay: 0.08 * i 33 | } // 单独控制退出时长 34 | }} 35 | transition={{ 36 | duration: 0.28, 37 | delay: 0.08 * i 38 | }} 39 | > 40 | {/* 分割线根据实际位置判断 */} 41 | {mains.findIndex(m => m.key === item.key) > 0 && <Divider />} 42 | 43 | <Flex justify='space-between' align="center" gap={8}> 44 | <Text>{item.label}</Text> 45 | {typeof item.change === 'function' ? ( 46 | <Switch 47 | loading={item.loading} 48 | defaultValue={item.defaultvalue} 49 | value={item.value as boolean} 50 | onChange={item.change} /> 51 | ) : ( 52 | <div>{item.change}</div> 53 | )} 54 | </Flex> 55 | </motion.div> 56 | ); 57 | })} 58 | </AnimatePresence> 59 | ); 60 | 61 | // 在父组件中使用React.memo 62 | export default React.memo(Content) -------------------------------------------------------------------------------- /public/tauri.svg: -------------------------------------------------------------------------------- 1 | <svg width="206" height="231" viewBox="0 0 206 231" fill="none" xmlns="http://www.w3.org/2000/svg"> 2 | <path d="M143.143 84C143.143 96.1503 133.293 106 121.143 106C108.992 106 99.1426 96.1503 99.1426 84C99.1426 71.8497 108.992 62 121.143 62C133.293 62 143.143 71.8497 143.143 84Z" fill="#FFC131"/> 3 | <ellipse cx="84.1426" cy="147" rx="22" ry="22" transform="rotate(180 84.1426 147)" fill="#24C8DB"/> 4 | <path fill-rule="evenodd" clip-rule="evenodd" d="M166.738 154.548C157.86 160.286 148.023 164.269 137.757 166.341C139.858 160.282 141 153.774 141 147C141 144.543 140.85 142.121 140.558 139.743C144.975 138.204 149.215 136.139 153.183 133.575C162.73 127.404 170.292 118.608 174.961 108.244C179.63 97.8797 181.207 86.3876 179.502 75.1487C177.798 63.9098 172.884 53.4021 165.352 44.8883C157.82 36.3744 147.99 30.2165 137.042 27.1546C126.095 24.0926 114.496 24.2568 103.64 27.6274C92.7839 30.998 83.1319 37.4317 75.8437 46.1553C74.9102 47.2727 74.0206 48.4216 73.176 49.5993C61.9292 50.8488 51.0363 54.0318 40.9629 58.9556C44.2417 48.4586 49.5653 38.6591 56.679 30.1442C67.0505 17.7298 80.7861 8.57426 96.2354 3.77762C111.685 -1.01901 128.19 -1.25267 143.769 3.10474C159.348 7.46215 173.337 16.2252 184.056 28.3411C194.775 40.457 201.767 55.4101 204.193 71.404C206.619 87.3978 204.374 103.752 197.73 118.501C191.086 133.25 180.324 145.767 166.738 154.548ZM41.9631 74.275L62.5557 76.8042C63.0459 72.813 63.9401 68.9018 65.2138 65.1274C57.0465 67.0016 49.2088 70.087 41.9631 74.275Z" fill="#FFC131"/> 5 | <path fill-rule="evenodd" clip-rule="evenodd" d="M38.4045 76.4519C47.3493 70.6709 57.2677 66.6712 67.6171 64.6132C65.2774 70.9669 64 77.8343 64 85.0001C64 87.1434 64.1143 89.26 64.3371 91.3442C60.0093 92.8732 55.8533 94.9092 51.9599 97.4256C42.4128 103.596 34.8505 112.392 30.1816 122.756C25.5126 133.12 23.9357 144.612 25.6403 155.851C27.3449 167.09 32.2584 177.598 39.7906 186.112C47.3227 194.626 57.153 200.784 68.1003 203.846C79.0476 206.907 90.6462 206.743 101.502 203.373C112.359 200.002 122.011 193.568 129.299 184.845C130.237 183.722 131.131 182.567 131.979 181.383C143.235 180.114 154.132 176.91 164.205 171.962C160.929 182.49 155.596 192.319 148.464 200.856C138.092 213.27 124.357 222.426 108.907 227.222C93.458 232.019 76.9524 232.253 61.3736 227.895C45.7948 223.538 31.8055 214.775 21.0867 202.659C10.3679 190.543 3.37557 175.59 0.949823 159.596C-1.47592 143.602 0.768139 127.248 7.41237 112.499C14.0566 97.7497 24.8183 85.2327 38.4045 76.4519ZM163.062 156.711L163.062 156.711C162.954 156.773 162.846 156.835 162.738 156.897C162.846 156.835 162.954 156.773 163.062 156.711Z" fill="#24C8DB"/> 6 | </svg> 7 | -------------------------------------------------------------------------------- /src/language/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "Title": "System Theme Auto Switch", 3 | "main": { 4 | "open": "Enable", 5 | "language": "Language", 6 | "citiTitle": "City", 7 | "citiError": "No city selected!", 8 | "citiPlaceholder": "Search by city name", 9 | "switchStyemMode": "Theme Background", 10 | "switchStyemModeTip": "Auto switch background when system theme changes", 11 | "switchStyemModeOpenTip": "When enabled, opens system settings when applying theme", 12 | "winBgEffect": "Window Background", 13 | "Mica": "Mica", 14 | "Acrylic": "Acrylic", 15 | "Default": "Default", 16 | "TabsOptionA": "Sunrise to Sunset", 17 | "TabsOptionAError": "Failed to get sunrise/sunset info!", 18 | "TabsOptionB": "Daytime", 19 | "Autostart": "Start with System", 20 | "AutostartTip": "Find Auto Theme Mode in settings, click to go to settings", 21 | "AutostartBtn": "Go to Settings", 22 | "StartShow": "Show Window on Startup", 23 | "deviationTitle": "Time Offset", 24 | "deviationPrompt": "Adjust daytime offset (minutes) - delay sunrise, advance sunset" 25 | }, 26 | "upModal": { 27 | "textA": [ 28 | "Checking:", 29 | "New version found", 30 | "Already latest version" 31 | ], 32 | "textB": [ 33 | "Updating...", 34 | "View Update", 35 | "Check Update" 36 | ], 37 | "title": "New version available:", 38 | "cancelText": "Cancel", 39 | "okText": "Update Now", 40 | "upData": "Update Log", 41 | "noText": "Skip This Update" 42 | }, 43 | "reviewModal": { 44 | "title": "Like this app?", 45 | "text": "Your rating matters! Would you like to rate us on Microsoft Store?", 46 | "laterText": "Later", 47 | "cancelText": "Don't Remind", 48 | "okText": "Rate Now" 49 | }, 50 | "quit": "Quit", 51 | "show": "Show Window", 52 | "Time": "Time", 53 | "dark": "Dark", 54 | "light": "Light", 55 | "switch": "Switch to", 56 | "ThemeDark": "Dark Theme", 57 | "ThemeLight": "Light Theme", 58 | "doc": [ 59 | "Weather", 60 | "About", 61 | "Developed by: Tuyang - Completely open source and free", 62 | "Get Help:" 63 | ], 64 | "themeName": { 65 | "spotlight": "Windows Spotlight, Dynamic images", 66 | "dark": "Windows Dark", 67 | "themeA": "Glow", 68 | "themeB": "Motion Capture", 69 | "themeD": "Fluent", 70 | "themeC": "Sunrise", 71 | "Custom": "Custom" 72 | } 73 | } -------------------------------------------------------------------------------- /PRIVACY.md: -------------------------------------------------------------------------------- 1 | 隐私策略 2 | 3 | **生效日期:** 2025年6月11日 4 | **开发者/发布者:** tuyangJs 5 | 6 | **感谢您使用 Windows AutoTheme!** 我们深知隐私的重要性。本策略明确说明应用如何收集、使用和保护您的信息。 7 | 8 | ## 1. 收集的信息及其用途 9 | 为实现**根据地理位置自动切换主题**的核心功能,应用会收集以下信息: 10 | 11 | * **IP地址:** 12 | * **收集方式:** 应用会向受信任的第三方服务商发送网络请求以获取您的**公共IP地址**。 13 | * **用途:** 仅用于估算您所在的**大致地理位置(城市/地区级别)**,以便确定当地的日出日落时间。**不会**用于精确定位或追踪。 14 | * **存储:** 应用本身**不会在您的设备本地长期存储您的完整IP地址**。获取到关联的城市信息后即丢弃原始IP地址。 15 | * **估算的城市/地区信息:** 16 | * **来源:** 基于您的IP地址从第三方服务获取。 17 | * **用途:** 用于向日出日落时间API查询该地区的准确日出日落时间。 18 | * **本地存储:** 您设定的城市/地区名称会**存储在设备本地**(如应用设置),用于后续自动更新日落日出时间,避免频繁查询。您可以随时在应用内清除或更改此设置。 19 | * **日出日落时间:** 20 | * **来源:** 通过调用第三方日出日落时间API。 21 | * **用途:** 核心功能依赖 - 用于在日出时自动切换至亮色主题,日落时自动切换至暗色主题。 22 | * **本地存储:** 获取的日出日落时间会**存储在设备本地**一段时间(例如缓存24小时),以减少网络请求次数。过期后会自动重新获取。 23 | 24 | ## 2. 网络连接说明 25 | * 应用**需要访问互联网**以执行以下功能: 26 | 1. 通过第三方服务查询与您IP地址关联的**大致地理位置(城市/地区)**。 27 | 2. 向**日出日落时间API**查询您所在城市的当日准确日出日落时间。 28 | * 这些网络请求是**实现自动根据当地日落日出时间切换主题功能所必需的**。 29 | 30 | ## 3. 数据共享与第三方服务 31 | 应用依赖以下第三方服务,向其发送必要数据以获取功能所需信息: 32 | 33 | * **IP地理位置服务提供商:** 34 | * **发送数据:** 您的公共IP地址。 35 | * **获取数据:** 与该IP地址关联的大致城市/地区信息。 36 | * **服务商:**和风天气:** 37 | * **隐私政策链接:** `https://www.qweather.com/terms/privacy` 38 | * **日出日落时间API提供商:** 39 | * **发送数据:** 估算出的城市名称和经纬度(或仅城市/地区名,取决于API要求)、查询日期。 40 | * **获取数据:** 指定地点和日期的日出、日落、黄昏等时间数据。 41 | * **服务商示例:** `https://www.qweather.com/weather`** 42 | * **隐私政策链接:** `https://www.qweather.com/terms/privacy` 43 | 44 | **重要提示:** 45 | * 应用**不会**将您的IP地址、位置信息或日出日落时间**主动出售、出租或共享**给除上述必要服务商之外的任何其他第三方用于广告或营销目的。 46 | * 我们对**第三方服务商**如何收集、处理您的数据(特别是IP地址)**没有直接控制权**。我们强烈建议您查阅其隐私政策: 47 | * [和风天气] 隐私政策:`[https://www.qweather.com/terms/privacy]` 48 | * 49 | 50 | ## 4. 本地存储的数据 51 | 应用会在您的设备本地存储以下信息: 52 | * 您选择的城市/地区名称(如果手动设置或从IP解析后保存)。 53 | * 最近获取的日出日落时间数据(缓存以提高效率)。 54 | * 您的应用偏好设置(如是否启用自动切换、主题切换的具体规则等)。 55 | * **这些本地数据不会被加密上传或同步到任何云端服务器。** 56 | 57 | ## 5. 用户控制与选择 58 | * **位置来源:** 您通常可以在应用设置中选择: 59 | * **自动获取(通过IP):** 应用自动获取大致位置。 60 | * **手动设置:** 您自行输入城市名称。 61 | * **清除数据:** 您可以通过卸载应用或在应用内重置设置来清除本地存储的所有数据(包括保存的城市和缓存的时间)。 62 | 63 | ## 6. 数据安全 64 | * 我们采取合理措施保护本地存储的数据。 65 | * 网络传输:应用与第三方API的通信**应使用HTTPS协议**(确保您调用的API支持HTTPS)。 66 | * **请注意:** 任何互联网传输都存在固有风险,我们无法保证100%的安全性。 67 | 68 | ## 7. 无个人身份信息收集 69 | * 应用**不会**刻意收集或要求您提供任何可识别个人身份的信息(PII),如姓名、地址、电子邮件、电话号码、精确位置(GPS)、设备唯一标识符、账户信息、联系人、文件内容等。 70 | * 收集的IP地址用于获取大致位置后即丢弃,且未与任何其他个人数据关联存储。 71 | 72 | ## 8. 无广告与分析 73 | * 本程序 **不包含**任何广告。 74 | * 本程序 **不使用**任何第三方分析或追踪SDK监控用户行为。 75 | 76 | ## 9. 儿童隐私 77 | 应用并非针对13岁以下儿童设计,也不会故意收集其个人信息。如有疑虑,请联系我们。 78 | 79 | ## 10. 隐私政策变更 80 | 更新后的政策将公布于此页面并更新生效日期。重大变更将通过应用内通知(如可行)或GitHub仓库公告告知。 81 | -------------------------------------------------------------------------------- /src/language/ru.json: -------------------------------------------------------------------------------- 1 | { 2 | "Title": "Автосмена Темы Системы", 3 | "main": { 4 | "open": "Включить", 5 | "language": "Язык", 6 | "citiTitle": "Город", 7 | "citiError": "Город не выбран!", 8 | "citiPlaceholder": "Поиск по городу", 9 | "switchStyemMode": "Фон Теми", 10 | "switchStyemModeTip": "Автосмена фона при изменении темы системы", 11 | "switchStyemModeOpenTip": "При включении открывает настройки системы при применении темы", 12 | "winBgEffect": "Материал Фона", 13 | "Mica": "Слюда", 14 | "Acrylic": "Акрил", 15 | "Default": "По умолчанию", 16 | "TabsOptionA": "Восход до Заката", 17 | "TabsOptionAError": "Ошибка получения информации о восходе/закате!", 18 | "TabsOptionB": "Дневное Время", 19 | "Autostart": "Запуск с Системой", 20 | "AutostartTip": "Найдите Auto Theme Mode в настройках, кликните для перехода", 21 | "AutostartBtn": "В Настройки", 22 | "StartShow": "Показывать Окно при Запуске", 23 | "deviationTitle": "Смещение Времени", 24 | "deviationPrompt": "Настройка смещения дневного времени (минуты) - задержать восход, ускорить закат" 25 | }, 26 | "upModal": { 27 | "textA": [ 28 | "Проверка:", 29 | "Найдена новая версия", 30 | "Уже последняя версия" 31 | ], 32 | "textB": [ 33 | "Обновление...", 34 | "Посмотреть Обновление", 35 | "Проверить Обновление" 36 | ], 37 | "title": "Доступна новая версия:", 38 | "cancelText": "Отмена", 39 | "okText": "Обновить", 40 | "upData": "История Обновлений", 41 | "noText": "Пропустить Это Обновление" 42 | }, 43 | "reviewModal": { 44 | "title": "Нравится это приложение?", 45 | "text": "Ваша оценка важна! Не могли бы вы оценить нас в Microsoft Store?", 46 | "laterText": "Позже", 47 | "cancelText": "Не Напоминать", 48 | "okText": "Оценить" 49 | }, 50 | "quit": "Выйти", 51 | "show": "Показать Окно", 52 | "Time": "Время", 53 | "dark": "Тёмная", 54 | "light": "Светлая", 55 | "switch": "Переключить на", 56 | "ThemeDark": "Тёмная Тема", 57 | "ThemeLight": "Светлая Тема", 58 | "doc": [ 59 | "Погода", 60 | "О программе", 61 | "Разработано: Tuyang - Полностью открытый исходный код и бесплатно", 62 | "Получить помощь:" 63 | ], 64 | "themeName": { 65 | "spotlight": "Windows Spotlight, Динамические изображения", 66 | "dark": "Windows Тёмная", 67 | "themeA": "Свечение", 68 | "themeB": "Захват Движения", 69 | "themeD": "Плавный", 70 | "themeC": "Восход", 71 | "Custom": "Пользовательская" 72 | } 73 | } -------------------------------------------------------------------------------- /src/language/es-ES.json: -------------------------------------------------------------------------------- 1 | { 2 | "Title": "Tema del Sistema Auto", 3 | "main": { 4 | "open": "Activar", 5 | "language": "Idioma", 6 | "citiTitle": "Ciudad", 7 | "citiError": "¡No hay ciudad seleccionada!", 8 | "citiPlaceholder": "Buscar por ciudad", 9 | "switchStyemMode": "Fondo del Tema", 10 | "switchStyemModeTip": "Cambiar fondo automáticamente al cambiar tema del sistema", 11 | "switchStyemModeOpenTip": "Al activar, abre configuración del sistema al aplicar tema", 12 | "winBgEffect": "Material de Fondo", 13 | "Mica": "Mica", 14 | "Acrylic": "Acrílico", 15 | "Default": "Predeterminado", 16 | "TabsOptionA": "Amanecer a Atardecer", 17 | "TabsOptionAError": "¡Error al obtener información de amanecer/atardecer!", 18 | "TabsOptionB": "Horario Diurno", 19 | "Autostart": "Iniciar con Sistema", 20 | "AutostartTip": "Encuentra Auto Theme Mode en ajustes, clic para ir a ajustes", 21 | "AutostartBtn": "Ir a Ajustes", 22 | "StartShow": "Mostrar Ventana al Iniciar", 23 | "deviationTitle": "Desviación de Tiempo", 24 | "deviationPrompt": "Ajustar desviación horaria (minutos) - retrasar amanecer, adelantar atardecer" 25 | }, 26 | "upModal": { 27 | "textA": [ 28 | "Verificando:", 29 | "Nueva versión encontrada", 30 | "Ya es la última versión" 31 | ], 32 | "textB": [ 33 | "Actualizando...", 34 | "Ver Actualización", 35 | "Comprobar Actualización" 36 | ], 37 | "title": "Nueva versión disponible:", 38 | "cancelText": "Cancelar", 39 | "okText": "Actualizar Ahora", 40 | "upData": "Registro de Actualizaciones", 41 | "noText": "Omitir Esta Actualización" 42 | }, 43 | "reviewModal": { 44 | "title": "¿Te gusta esta app?", 45 | "text": "¡Tu calificación es importante! ¿Podrías calificarnos en Microsoft Store?", 46 | "laterText": "Después", 47 | "cancelText": "No Recordar", 48 | "okText": "Calificar Ahora" 49 | }, 50 | "quit": "Salir", 51 | "show": "Mostrar Ventana", 52 | "Time": "Tiempo", 53 | "dark": "Oscuro", 54 | "light": "Claro", 55 | "switch": "Cambiar a", 56 | "ThemeDark": "Tema Oscuro", 57 | "ThemeLight": "Tema Claro", 58 | "doc": [ 59 | "Clima", 60 | "Acerca de", 61 | "Desarrollado por: Tuyang - Completamente código abierto y gratuito", 62 | "Obtener Ayuda:" 63 | ], 64 | "themeName": { 65 | "spotlight": "Windows Spotlight, Imágenes dinámicas", 66 | "dark": "Windows Oscuro", 67 | "themeA": "Resplandor", 68 | "themeB": "Captura de Movimiento", 69 | "themeD": "Fluido", 70 | "themeC": "Amanecer", 71 | "Custom": "Personalizado" 72 | } 73 | } -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | body { 2 | overflow: hidden; 3 | user-select: none; 4 | text-rendering: optimizeLegibility; 5 | margin: 0; 6 | } 7 | 8 | h1, 9 | h2, 10 | h3, 11 | h4, 12 | h5 { 13 | margin: 6px 0 !important; 14 | } 15 | 16 | #root { 17 | --shadow: 0 6px 16px 0 #dc7a0314, 0 3px 6px -4px #dc7a031f, 0 9px 28px 8px #dc7a030d; 18 | } 19 | 20 | .drag-region { 21 | -webkit-app-region: drag; 22 | /* 设置此区域为可拖动 */ 23 | flex: 1; 24 | padding-inline: 6px; 25 | padding-block: 4px; 26 | border-bottom: 1px solid #000000; 27 | } 28 | 29 | .ant-radio-group { 30 | display: unset !important; 31 | } 32 | 33 | .logo { 34 | width: 26px; 35 | 36 | } 37 | 38 | .ant-divider { 39 | margin: 8px 0px 40 | } 41 | 42 | .ant-layout { 43 | height: 100vh; 44 | padding: 8px; 45 | } 46 | 47 | button.titlebar { 48 | height: auto; 49 | padding-inline: 10px !important; 50 | } 51 | 52 | .ant-btn { 53 | border: 0px solid transparent; 54 | } 55 | 56 | .ant-btn .ant-btn-icon{ 57 | line-height: 19px; 58 | } 59 | .ant-segmented-group { 60 | gap: 2px; 61 | } 62 | 63 | .ant-segmented-item-label, 64 | .segmented-xs { 65 | /* min-height: 24px !important; 66 | line-height: 24px !important; 67 | padding: 0 7px !important; */ 68 | } 69 | 70 | 71 | .ant-typography { 72 | margin-bottom: 1px !important 73 | } 74 | 75 | button:hover { 76 | box-shadow: var(--shadow) !important; 77 | } 78 | 79 | input:focus { 80 | box-shadow: var(--shadow); 81 | } 82 | 83 | .ant-picker.ant-picker-range.ant-picker-focused { 84 | box-shadow: var(--shadow); 85 | } 86 | 87 | .ant-radio-button-wrapper.ant-radio-button-wrapper-checked:hover { 88 | box-shadow: var(--shadow); 89 | } 90 | 91 | .ant-switch.ant-switch-checked:hover { 92 | box-shadow: var(--shadow); 93 | } 94 | 95 | .container { 96 | margin: 0; 97 | /* margin-block-start: 38px; */ 98 | margin-inline: 12px; 99 | display: flex; 100 | flex-direction: column; 101 | text-align: center; 102 | } 103 | :root { 104 | --sb-size: 6px; 105 | /* 滚动条宽度 */ 106 | --sb-track: rgba(0, 0, 0, 0.04); 107 | /* 轨道 */ 108 | --sb-thumb-color: transparent; 109 | /* thumb(Firefox fallback) */ 110 | --sb-thumb-radius: 10px; 111 | } 112 | 113 | /* WebKit / Chromium / Safari */ 114 | *::-webkit-scrollbar { 115 | width: var(--sb-size); 116 | height: var(--sb-size); 117 | } 118 | 119 | *::-webkit-scrollbar-track { 120 | background: transparent; 121 | border-radius: var(--sb-thumb-radius); 122 | } 123 | 124 | *::-webkit-scrollbar-track:hover { 125 | background: var(--sb-track); 126 | border-radius: var(--sb-thumb-radius); 127 | } 128 | 129 | *::-webkit-scrollbar-thumb { 130 | border-radius: var(--sb-thumb-radius); 131 | background: #6d6d6d 132 | /* 可以用渐变 */ 133 | } 134 | 135 | *::-webkit-scrollbar-thumb:hover { 136 | filter: brightness(0.95); 137 | } 138 | -------------------------------------------------------------------------------- /src/mod/ThemeConfig.ts: -------------------------------------------------------------------------------- 1 | import { ThemeConfig, theme } from "antd"; 2 | import { useMemo } from "react"; 3 | import { AppDataType } from "../Type"; 4 | import { platform, version } from '@tauri-apps/plugin-os'; 5 | async function isWindows11() { 6 | 7 | // 判断平台是否为 Windows 8 | if ((await platform()).toLowerCase() !== 'windows') { 9 | return false; 10 | } 11 | 12 | // 获取操作系统版本号,例如 "10.0.22000.1" 13 | const osVersion = await version(); 14 | const parts = osVersion.split('.'); 15 | console.log(parts, osVersion); 16 | // 检查版本号前三位是否符合条件,并判断构建号是否大于等于 22000 17 | if (parts.length >= 3 && parts[0] === '10' && parts[1] === '0') { 18 | const build = parseInt(parts[2], 10); 19 | return build >= 22000; 20 | } 21 | 22 | return false; 23 | } 24 | 25 | 26 | const isWin11 = await isWindows11() 27 | 28 | 29 | const ThemeFun = (themeDack: boolean, winBgEffect: AppDataType['winBgEffect'] | undefined) => { 30 | //背景渲染 31 | let BgLayout = 'transparent' 32 | let headerBg = themeDack ? '#22222280' : '#ffffff4d' 33 | winBgEffect = isWin11 ? winBgEffect : 'Default' 34 | switch (winBgEffect) { 35 | case 'Acrylic': 36 | BgLayout = themeDack ? 'linear-gradient(33deg, #121317c4, #323b4296)' : 'linear-gradient(33deg, #F0EFF0c4, #FAF8F996)' 37 | headerBg = themeDack ? '#222222bf' : '#ffffffbf' 38 | break; 39 | case 'Default': 40 | headerBg = isWin11 ? 'transparent' : (themeDack ? '#180d00' : '#fdf0e6') 41 | BgLayout = themeDack ? 'linear-gradient(33deg, #121317, #323b42)' : 'linear-gradient(33deg, #fff7e9, #e8e8e8)' 42 | break 43 | } 44 | //主题渲染配置 45 | const Themeconfig: ThemeConfig = useMemo(() => ({ 46 | algorithm: themeDack ? theme.darkAlgorithm : theme.defaultAlgorithm, 47 | components: { 48 | Divider: { 49 | colorSplit: themeDack ? '#83838329' : '#85858529' 50 | }, 51 | Segmented: { 52 | trackBg: themeDack ? '#87878745' : '#bfbfbf45', 53 | itemSelectedBg: themeDack ? '#23232391' : '#ffffff91', 54 | }, 55 | Layout: { 56 | headerBg: headerBg, 57 | } 58 | }, 59 | token: { 60 | borderRadius:14, 61 | borderRadiusOuter:16, 62 | colorPrimary: '#ff8c00', 63 | colorBgLayout: BgLayout, 64 | colorBgBase: themeDack ? '#00000096' : '#ffffff96', 65 | colorBorder: themeDack ? '#87878796' : '#bfbfbf96', 66 | colorBgElevated: themeDack ? '#313131' : '#ffffff', 67 | colorBgSpotlight: '#313131', 68 | }, 69 | }), [themeDack, BgLayout]); 70 | const antdToken = useMemo(() => theme.getDesignToken(Themeconfig), [Themeconfig]); //主题渲染 71 | return { Themeconfig, antdToken } 72 | } 73 | 74 | export { ThemeFun, isWin11 } 75 | -------------------------------------------------------------------------------- /src/mod/utils/tauri-file.ts: -------------------------------------------------------------------------------- 1 | // tauri-file.ts 2 | import { useEffect, useState } from 'react'; 3 | import { readFile } from '@tauri-apps/plugin-fs'; 4 | 5 | // module-level cache:path -> objectUrl 6 | const objectUrlCache = new Map<string, string>(); 7 | // reference count optional: path -> count, 用于在没人用时 revoke(可选) 8 | const refCount = new Map<string, number>(); 9 | 10 | function guessImageMime(path?: string): string { 11 | if (!path) return 'application/octet-stream'; 12 | const ext = path.split('.').pop()?.toLowerCase(); 13 | if (!ext) return 'application/octet-stream'; 14 | if (['jpg','jpeg'].includes(ext)) return 'image/jpeg'; 15 | if (['png'].includes(ext)) return 'image/png'; 16 | if (['gif'].includes(ext)) return 'image/gif'; 17 | if (['webp'].includes(ext)) return 'image/webp'; 18 | return 'application/octet-stream'; 19 | } 20 | 21 | /** 22 | * Hook: 返回 src (string) —— 若 path 相同会复用已存在的 object URL,避免闪烁。 23 | */ 24 | export function useLocalImageUrl(path?: string): { src: string; loading: boolean; error?: Error | null } { 25 | const [src, setSrc] = useState<string>(() => (path ? (objectUrlCache.get(path) ?? '') : '')); 26 | const [loading, setLoading] = useState<boolean>(() => (path ? !objectUrlCache.has(path) : false)); 27 | const [error, setError] = useState<Error | null>(null); 28 | 29 | useEffect(() => { 30 | let mounted = true; 31 | if (!path) { 32 | setSrc(''); 33 | setLoading(false); 34 | setError(null); 35 | return; 36 | } 37 | 38 | // 如果缓存命中,直接使用且不触发读取 39 | const cached = objectUrlCache.get(path); 40 | if (cached) { 41 | setSrc(cached); 42 | setLoading(false); 43 | setError(null); 44 | // bump refcount 45 | refCount.set(path, (refCount.get(path) || 0) + 1); 46 | return; 47 | } 48 | 49 | setLoading(true); 50 | (async () => { 51 | try { 52 | // readFile 返回 Uint8Array-like 53 | const data = await readFile(path); 54 | const uint8 = data instanceof Uint8Array ? data : new Uint8Array(data as any); 55 | const mime = guessImageMime(path); 56 | const blob = new Blob([uint8], { type: mime }); 57 | const url = URL.createObjectURL(blob); 58 | // cache & refcount 59 | objectUrlCache.set(path, url); 60 | refCount.set(path, (refCount.get(path) || 0) + 1); 61 | 62 | if (mounted) { 63 | setSrc(url); 64 | setLoading(false); 65 | setError(null); 66 | } 67 | } catch (e) { 68 | console.error('useLocalImageUrl read failed', e); 69 | if (mounted) { 70 | setError(e as Error); 71 | setLoading(false); 72 | } 73 | } 74 | })(); 75 | 76 | return () => { 77 | mounted = false; 78 | // release one refCount — 不立即 revoke URL(避免短时间切换导致重复创建) 79 | const count = refCount.get(path) || 0; 80 | if (count <= 1) { 81 | refCount.delete(path); 82 | // optional: 延迟 revoke,或在全局清理时 revoke 83 | // const url = objectUrlCache.get(path); 84 | // if (url) { URL.revokeObjectURL(url); objectUrlCache.delete(path); } 85 | } else { 86 | refCount.set(path, count - 1); 87 | } 88 | }; 89 | }, [path]); 90 | 91 | return { src, loading, error }; 92 | } 93 | -------------------------------------------------------------------------------- /README-English.md: -------------------------------------------------------------------------------- 1 | # Windows AutoTheme 2 | <div align="center"> 3 | <img src="https://github.com/user-attachments/assets/c3cdbcf6-6bdc-4e91-a84a-55ef109c60f5" alt="Screenshot 1" width="100%"> 4 | 5 | #### Language: [中文](/README.md) | [English](/README-English.md) 6 | 7 | </div> 8 | 9 | ## Overview 10 | 11 | **Windows AutoTheme** is a lightweight utility that automatically switches your Windows theme based on the time of day. In accordance with Windows’ official guidelines, “light mode” features a predominantly light (often white) background with dark text for optimal clarity, while “dark mode” uses a dark background paired with light text to reduce eye strain in low-light conditions. With Windows AutoTheme, your system uses light mode during the day and seamlessly transitions to dark mode at night. The backend is built in Rust for efficient system operations, and the frontend is developed using TypeScript and Ant Design 5 to deliver a modern, visually appealing interface. Additionally, the tool utilizes a built-in free API to fetch sunrise and sunset data, enabling intelligent and automated theme switching. 12 | 13 | --- 14 | 15 | ## 🛠️ Installation Methods 16 | ### Recommended Installation (Microsoft Store): 17 | [![Get the app](https://get.microsoft.com/images/en-us%20light.svg)](https://apps.microsoft.com/detail/9n7nd584tdv1) 18 | 19 | ### Traditional Installation: 20 | >Open our [releases page](https://github.com/tuyangJs/Windows_AutoTheme/releases) and download the latest version of the installer package. 21 | ## Key Features 22 | 23 | - **Automatic Theme Switching:** Dynamically toggles between light and dark modes—light mode for daytime and dark mode for nighttime—based on official Windows theme definitions. 24 | - **Efficient and Lightweight:** Utilizes Rust for high-performance system calls, ensuring stable and responsive operation. 25 | - **Modern User Interface:** The frontend is built with TypeScript and a customized Ant Design 5 interface, offering a clean, modern, and attractive design. 26 | - **Free Astronomical Data Integration:** Integrates a free API to retrieve real-time sunrise and sunset times, guiding the automatic theme transitions. 27 | 28 | --- 29 | 30 | ## Screenshots 31 | 32 | <div align="center"> 33 | <img src="https://github.com/user-attachments/assets/8ed6411d-cc19-4884-a2b6-8d0d65f64078" alt="Screenshot 2" width="55%"> 34 | </div> 35 | 36 | --- 37 | 38 | ## Development and Debugging 39 | 40 | To set up the project for development: 41 | 42 | 1. **Install Dependencies:** 43 | 44 | ``` 45 | npm install 46 | ``` 47 | 48 | 2. **Debug the Application:** 49 | 50 | ``` 51 | npm start 52 | ``` 53 | 54 | 3. **Build the Project:** 55 | ``` 56 | npm run tauri build 57 | ``` 58 | 59 | --- 60 | 61 | ## Additional Repositories 62 | 63 | - **Gitee (Recommended for China users):** [https://gitee.com/ilinxuan/windows_-auto-theme](https://gitee.com/ilinxuan/windows_-auto-theme) 64 | - **GitHub:** [https://github.com/tuyangJs/Windows_AutoTheme](https://github.com/tuyangJs/Windows_AutoTheme) 65 | 66 | --- 67 | 68 | ## Contact the Author 69 | 70 | - **QQ Group:** [703623743](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=IVNKPTJ9WqoIHHCsy7UMkQd16NLnfjeD&authKey=WVTDqfUgdv9oV0d8%2BZz5krS98IIlB1Kuvm%2BS3pfMU1H6FBCV1b2xoG5pWsggiAgt&noverify=0&group_code=703623743) 71 | - **Email:** [ihanlong@qq.com](ihanlong@qq.com) 72 | 73 | --- 74 | 75 | This translation not only conveys the technical details and features of Windows AutoTheme but also aligns with the official Windows descriptions of light and dark themes. 76 | -------------------------------------------------------------------------------- /src/updates.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Flex, Modal, Typography } from "antd"; 2 | import { useEffect, useState } from "react"; 3 | import Markdown from 'react-markdown' 4 | import { AppDataType } from "./Type"; 5 | import { UpdateType, checkForUpdates } from "./mod/update"; 6 | 7 | 8 | interface Props { 9 | version: string 10 | locale: any 11 | setData: any 12 | AppData: AppDataType 13 | } 14 | 15 | const { Text } = Typography; 16 | const Updates: React.FC<Props> = ({ version, locale, setData, AppData }) => { 17 | const [isModalOpen, setIsModalOpen] = useState(false); 18 | const [btnLoad, setBtnLoad] = useState(false); 19 | const [update, setUpdate] = useState<UpdateType | undefined>(); 20 | const nomitCancel = () => { 21 | setData({ Skipversion: update?.latestVersion }) 22 | setIsModalOpen(false); 23 | } 24 | const updates = () => { 25 | setBtnLoad(true) 26 | checkForUpdates(version).then((update) => { 27 | if (update) { 28 | setUpdate(update) 29 | if (update.latestVersion != AppData.Skipversion) { 30 | showModal() 31 | } 32 | } else { 33 | setUpdate(undefined) 34 | } 35 | setBtnLoad(false) 36 | }); 37 | } 38 | useEffect(updates, []) 39 | const showModal = () => { 40 | setIsModalOpen(true); 41 | }; 42 | 43 | const handleCancel = () => { 44 | setIsModalOpen(false); 45 | }; 46 | const onClickbtn = () => { 47 | if (update) { 48 | showModal() 49 | } else { 50 | updates() 51 | } 52 | } 53 | const { upModal } = locale || { upModal: null } 54 | return ( 55 | <> 56 | <Flex justify="center" align="center" gap={8}> 57 | 58 | { 59 | btnLoad ? <Text >{upModal?.textA[0]}</Text> : 60 | update ? ( 61 | <Text >{upModal?.textA[1]}</Text> 62 | ) : ( 63 | <Text type="secondary">{upModal?.textA[2]}</Text> 64 | ) 65 | } 66 | <Button 67 | onClick={onClickbtn} 68 | color={update ? "yellow" : "primary"} 69 | variant="link" 70 | loading={btnLoad}> 71 | {btnLoad ? upModal?.textB[0] : update ? upModal?.textB[1] : upModal?.textB[2]} 72 | </Button> 73 | </Flex> 74 | 75 | <Modal 76 | title={`${upModal?.title}${update?.latestVersion}`} 77 | open={isModalOpen} 78 | style={{ userSelect: 'text' }} 79 | styles={{body:{ 80 | maxHeight: '62vh', 81 | overflowY: 'auto' 82 | }}} 83 | centered 84 | footer={[ 85 | <Button 86 | key="nomit" 87 | type="default" 88 | onClick={nomitCancel}> 89 | {upModal?.noText} 90 | </Button>, 91 | <Button 92 | key="submit" 93 | type="primary" 94 | href="https://gitee.com/ilinxuan/windows_-auto-theme/releases/latest" 95 | target="_blank" 96 | onClick={handleCancel}> 97 | {upModal?.okText} (Gitee) 98 | </Button>, 99 | <Button 100 | key="link" 101 | href={update?.releaseUrl} 102 | target="_blank" 103 | type="primary" 104 | onClick={handleCancel} 105 | > 106 | {upModal?.okText} (GitHub) 107 | </Button>, 108 | ]} 109 | maskClosable={false} 110 | onCancel={handleCancel} 111 | > 112 | <Markdown>{`#### ${upModal?.upData} :\n ${update?.releaseNotes}`}</Markdown> 113 | </Modal></> 114 | ) 115 | } 116 | export { Updates } 117 | 118 | -------------------------------------------------------------------------------- /src/mod/RatingPrompt.tsx: -------------------------------------------------------------------------------- 1 | // src/components/RatingPrompt.tsx 2 | import { FC, useEffect, useState } from "react"; 3 | import { Button, Modal } from "antd"; 4 | import useAppData from "./DataSave"; 5 | import { openStoreRating } from "./openStoreRating"; 6 | 7 | interface RatingPromptProps { 8 | locale: any; 9 | } 10 | 11 | const RatingPrompt: FC<RatingPromptProps> = ({ locale }) => { 12 | const { AppData, updateRatingPrompt } = useAppData(); 13 | const [visible, setVisible] = useState(false); 14 | 15 | // 确保 ratingPrompt 对象存在且完整 16 | const ratingPrompt = AppData?.ratingPrompt || { 17 | lastPromptTime: 0, 18 | promptCount: 0, 19 | neverShowAgain: false, 20 | }; 21 | 22 | // 解构为原始值,避免对象引用导致的重复 effect 调用 23 | const { 24 | lastPromptTime = 0, 25 | promptCount = 0, 26 | neverShowAgain = false, 27 | } = ratingPrompt; 28 | 29 | useEffect(() => { 30 | if (neverShowAgain) return; 31 | 32 | const now = Date.now(); 33 | 34 | // 首次运行:当 promptCount === 0 且 lastPromptTime === 0 时 35 | // 把 lastPromptTime 初始化为现在,首次提示将在 24 小时后出现 36 | if (promptCount === 0 && lastPromptTime === 0) { 37 | // 只初始化,不弹窗 38 | updateRatingPrompt({ lastPromptTime: now, promptCount: 0 }); 39 | return; 40 | } 41 | 42 | const timeSinceLastPrompt = now - (lastPromptTime || 0); 43 | 44 | // 首次提示逻辑:24 小时后提示(如果 promptCount === 0 并且 lastPromptTime 已初始化) 45 | // 后续提示:至少间隔 7 天 46 | const shouldShow = 47 | promptCount === 0 48 | ? timeSinceLastPrompt > 24 * 3600 * 1000 49 | : timeSinceLastPrompt > 7 * 24 * 3600 * 1000; 50 | 51 | // 最多提示 3 次 52 | if (shouldShow && promptCount < 3) { 53 | const timer = setTimeout(() => { 54 | setVisible(true); 55 | // 在显示时增加提示计数并记录显示时间(用于下一轮间隔计算) 56 | updateRatingPrompt({ 57 | lastPromptTime: Date.now(), 58 | promptCount: promptCount + 1, 59 | }); 60 | }, 5000); // 应用启动后 5 秒显示 61 | 62 | return () => clearTimeout(timer); 63 | } 64 | }, [lastPromptTime, promptCount, neverShowAgain, updateRatingPrompt]); 65 | 66 | const handleChoice = (choice: "now" | "later" | "never") => { 67 | // 当用户选择“现在”或“稍后”时,更新 lastPromptTime(推迟下一次提示) 68 | const now = Date.now(); 69 | 70 | if (choice === "now") { 71 | // 打开 Microsoft Store 评分 72 | openStoreRating("review"); 73 | // 已经在显示时把 promptCount +1 了,这里只需更新 lastPromptTime(确保下一次间隔生效) 74 | updateRatingPrompt({ lastPromptTime: now }); 75 | } 76 | 77 | if (choice === "later") { 78 | // 标记稍后(把 lastPromptTime 更新为现在,这样下一次提示会按照后续间隔计算) 79 | updateRatingPrompt({ lastPromptTime: now }); 80 | } 81 | 82 | if (choice === "never") { 83 | // 标记不再提示 84 | updateRatingPrompt({ neverShowAgain: true }); 85 | } 86 | 87 | setVisible(false); 88 | }; 89 | 90 | if (!visible) return null; 91 | 92 | return ( 93 | <Modal 94 | title={locale?.reviewModal?.title} 95 | open={visible} 96 | onCancel={() => handleChoice("later")} 97 | footer={null} 98 | centered 99 | closable={false} 100 | maskClosable={false} 101 | className="rating-prompt-modal" 102 | > 103 | <div className="p-4"> 104 | <p className="mb-4 text-gray-700 text-center"> 105 | {locale?.reviewModal?.text} 106 | </p> 107 | <div className="flex justify-between"> 108 | <Button onClick={() => handleChoice("later")}> 109 | {locale?.reviewModal?.laterText} 110 | </Button> 111 | <Button onClick={() => handleChoice("never")}> 112 | {locale?.reviewModal?.cancelText} 113 | </Button> 114 | <Button type="primary" onClick={() => handleChoice("now")}> 115 | {locale?.reviewModal?.okText} 116 | </Button> 117 | </div> 118 | </div> 119 | </Modal> 120 | ); 121 | }; 122 | 123 | export default RatingPrompt; 124 | -------------------------------------------------------------------------------- /src-tauri/src/theme_apply.rs: -------------------------------------------------------------------------------- 1 | // theme_apply.rs 2 | use std::path::Path; 3 | use std::process::Command; 4 | 5 | /// 主题应用模块 6 | pub struct ThemeApplier; 7 | 8 | impl ThemeApplier { 9 | /// 直接运行主题文件(后台运行,不显示窗口)- 主要方法 10 | pub fn apply_theme_by_path(theme_path: &str) -> Result<(), String> { 11 | println!("正在后台应用主题: {}", theme_path); 12 | 13 | // 验证主题文件是否存在 14 | if !Path::new(theme_path).exists() { 15 | return Err(format!("主题文件不存在: {}", theme_path)); 16 | } 17 | 18 | // 验证文件扩展名 19 | if !theme_path.to_lowercase().ends_with(".theme") { 20 | return Err("文件不是有效的主题文件 (.theme)".to_string()); 21 | } 22 | 23 | // 方法1: 使用 start /b 在后台运行主题文件 24 | let result = Command::new("cmd") 25 | .args(&["/C", "start", "/b", "", theme_path]) 26 | .status(); 27 | 28 | match result { 29 | Ok(status) if status.success() => { 30 | println!("主题应用命令执行成功(后台运行)"); 31 | Ok(()) 32 | } 33 | Ok(status) => Err(format!("执行主题应用命令失败,退出码: {:?}", status.code())), 34 | Err(e) => { 35 | // 如果 start /b 失败,尝试其他方法 36 | println!("start /b 方法失败,尝试替代方法: {}", e); 37 | Self::apply_theme_alternative(theme_path) 38 | } 39 | } 40 | } 41 | 42 | /// 替代方法:使用 PowerShell 隐藏窗口 43 | pub fn apply_theme_alternative(theme_path: &str) -> Result<(), String> { 44 | println!("尝试使用 PowerShell 方法应用主题: {}", theme_path); 45 | let _ = Self::apply_theme_via_registry(theme_path); 46 | // 方法2: 使用 PowerShell 的 Start-Process 隐藏窗口 47 | let status = Command::new("powershell") 48 | .args(&[ 49 | "-Command", 50 | "Start-Process", 51 | "-FilePath", 52 | theme_path, 53 | "-WindowStyle", 54 | "Hidden", 55 | ]) 56 | .status() 57 | .map_err(|e| format!("执行 PowerShell 命令失败: {}", e))?; 58 | 59 | if status.success() { 60 | println!("主题静默应用成功"); 61 | Ok(()) 62 | } else { 63 | // 如果 PowerShell 也失败,尝试最基础的 cmd 方法 64 | println!("PowerShell 方法失败,尝试基础 cmd 方法"); 65 | Self::apply_theme_fallback(theme_path) 66 | } 67 | } 68 | 69 | /// 回退方法:使用最基础的 cmd 命令 70 | fn apply_theme_fallback(theme_path: &str) -> Result<(), String> { 71 | println!("使用基础 cmd 方法应用主题: {}", theme_path); 72 | 73 | // 方法3: 直接使用 cmd 运行(可能会显示窗口,但确保能工作) 74 | let status = Command::new("cmd") 75 | .args(&["/C", theme_path]) 76 | .status() 77 | .map_err(|e| format!("执行基础命令失败: {}", e))?; 78 | 79 | if status.success() { 80 | println!("主题应用成功(可能显示了窗口)"); 81 | Ok(()) 82 | } else { 83 | Err("所有应用主题的方法都失败了".to_string()) 84 | } 85 | } 86 | 87 | /// 通过注册表直接设置主题(最静默的方法) 88 | pub fn apply_theme_via_registry(theme_path: &str) -> Result<(), String> { 89 | use winreg::enums::*; 90 | use winreg::RegKey; 91 | 92 | println!("通过注册表应用主题: {}", theme_path); 93 | 94 | if !Path::new(theme_path).exists() { 95 | return Err(format!("主题文件不存在: {}", theme_path)); 96 | } 97 | 98 | let hkcu = RegKey::predef(HKEY_CURRENT_USER); 99 | let themes_key = hkcu 100 | .open_subkey_with_flags( 101 | "Software\\Microsoft\\Windows\\CurrentVersion\\Themes", 102 | KEY_WRITE, 103 | ) 104 | .map_err(|e| format!("无法打开注册表键: {}", e))?; 105 | 106 | // 设置当前主题 107 | themes_key 108 | .set_value("CurrentTheme", &theme_path.to_string()) 109 | .map_err(|e| format!("无法设置 CurrentTheme: {}", e))?; 110 | 111 | // 设置 ThemeMRU(最近使用的主题) 112 | let theme_mru = format!("{};", theme_path); 113 | themes_key 114 | .set_value("ThemeMRU", &theme_mru) 115 | .map_err(|e| format!("无法设置 ThemeMRU: {}", e))?; 116 | 117 | println!("注册表设置成功,主题将在下次登录或系统刷新时生效"); 118 | Ok(()) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/mod/DataSave.ts: -------------------------------------------------------------------------------- 1 | // src/hooks/useAppData.ts 2 | import { create } from "zustand"; 3 | import { persist } from "zustand/middleware"; 4 | import { AppDataType, RatingPromptType } from "../Type"; 5 | import { isEnabled } from "@tauri-apps/plugin-autostart"; 6 | import { isWin11 } from "./ThemeConfig"; 7 | const SystemStart = await isEnabled(); 8 | 9 | // 深度合并函数 10 | const deepMerge = (defaults: any, stored: any): any => { 11 | if (typeof stored !== 'object' || stored === null) { 12 | return stored !== undefined ? stored : defaults; 13 | } 14 | 15 | if (Array.isArray(stored)) { 16 | return stored; 17 | } 18 | 19 | const merged = { ...defaults }; 20 | for (const key of Object.keys(stored)) { 21 | if (Object.prototype.hasOwnProperty.call(stored, key)) { 22 | if ( 23 | typeof stored[key] === 'object' && 24 | stored[key] !== null && 25 | !Array.isArray(stored[key]) 26 | ) { 27 | merged[key] = deepMerge(defaults[key], stored[key]); 28 | } else { 29 | merged[key] = stored[key]; 30 | } 31 | } 32 | } 33 | return merged; 34 | }; 35 | // 默认评分提示状态 36 | const defaultRatingPrompt: RatingPromptType = { 37 | lastPromptTime: 0, 38 | promptCount: 0, 39 | neverShowAgain: false, 40 | }; 41 | 42 | // 默认应用数据配置 43 | const defaultAppData: AppDataType = { 44 | open: false, 45 | rcrl: false, 46 | city: { position: undefined, name: '' }, 47 | times: ["6:00", "18:00"], 48 | Autostart: SystemStart, 49 | language: undefined, 50 | StartShow: true, 51 | Skipversion: '', 52 | winBgEffect: isWin11 ? 'Mica' : 'Default', 53 | deviation: 15, 54 | rawTime: ["6:00", "18:00"], 55 | ratingPrompt: defaultRatingPrompt, 56 | StyemTheme: [], 57 | StyemThemeEnable: false 58 | }; 59 | 60 | interface AppDataStore { 61 | AppData: AppDataType; 62 | setData: (update: Partial<AppDataType>) => void; 63 | updateRatingPrompt: (update: Partial<RatingPromptType>) => void; 64 | } 65 | 66 | const useAppDataStore = create<AppDataStore>()( 67 | persist( 68 | (set) => ({ 69 | AppData: defaultAppData, 70 | 71 | setData: (update: Partial<AppDataType>) => { 72 | set((state) => { 73 | const prevData = state.AppData || defaultAppData; 74 | 75 | // 创建更新后的对象 76 | const updatedData = { 77 | ...prevData, 78 | ...update, 79 | }; 80 | 81 | // 确保ratingPrompt字段存在且结构正确 82 | if (!updatedData.ratingPrompt) { 83 | updatedData.ratingPrompt = { ...defaultRatingPrompt }; 84 | } 85 | 86 | // 确保language字段有效 87 | if (!updatedData.language || updatedData.language === '') { 88 | updatedData.language = 'en_US'; 89 | } 90 | 91 | return { AppData: updatedData as AppDataType }; 92 | }); 93 | }, 94 | 95 | updateRatingPrompt: (update: Partial<RatingPromptType>) => { 96 | set((state) => { 97 | const prevData = state.AppData || defaultAppData; 98 | 99 | // 创建更新后的对象 100 | const updatedData = { 101 | ...prevData, 102 | ratingPrompt: { 103 | ...(prevData.ratingPrompt || defaultRatingPrompt), 104 | ...update 105 | } 106 | }; 107 | 108 | return { AppData: updatedData as AppDataType }; 109 | }); 110 | }, 111 | }), 112 | { 113 | name: 'AppData', 114 | // 使用自定义的合并逻辑来替换默认的浅合并 115 | merge: (persistedState, currentState) => { 116 | if (typeof persistedState === 'object' && persistedState !== null) { 117 | const merged = deepMerge(currentState, persistedState); 118 | 119 | // 确保ratingPrompt结构正确 120 | if (!merged.AppData.ratingPrompt) { 121 | merged.AppData.ratingPrompt = { ...defaultRatingPrompt }; 122 | } else { 123 | merged.AppData.ratingPrompt = { 124 | ...defaultRatingPrompt, 125 | ...merged.AppData.ratingPrompt 126 | }; 127 | } 128 | 129 | // 确保language字段有效,防止空字符串key导致的问题 130 | if (!merged.AppData.language || merged.AppData.language === '') { 131 | merged.AppData.language = 'en_US'; 132 | } 133 | 134 | return merged; 135 | } 136 | return currentState; 137 | }, 138 | // 可选的版本控制,用于未来的数据迁移 139 | version: 1, 140 | } 141 | ) 142 | ); 143 | 144 | // 保持原有API结构的hook 145 | const useAppData = () => { 146 | const { AppData, setData, updateRatingPrompt } = useAppDataStore(); 147 | 148 | return { 149 | AppData, 150 | setData, 151 | updateRatingPrompt 152 | }; 153 | }; 154 | 155 | export default useAppData; -------------------------------------------------------------------------------- /src/language/index.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Dropdown } from "antd"; 2 | import type { AppDataType } from '../Type'; 3 | import { useEffect, useState } from "react"; 4 | import { ItemType } from "antd/es/menu/interface"; 5 | 6 | interface LanguageItem { 7 | key: string; 8 | label: string; 9 | } 10 | 11 | // 语言菜单项 12 | export const languageItems: LanguageItem[] = [ 13 | { key: 'zh_CN', label: "简体中文" }, 14 | { key: 'zh_HK', label: "繁体中文" }, 15 | { key: 'en', label: "English" }, 16 | { key: 'es', label: "Español" }, 17 | { key: 'ja', label: "日本語" }, 18 | { key: 'ru', label: "Russian" }, 19 | ]; 20 | 21 | // 支持的语言代码列表 22 | const SUPPORTED_LANGUAGES = languageItems.map(item => item.key); 23 | 24 | // 语言包加载器 25 | const localeLoaders: Record<string, () => Promise<{ default: any }>> = { 26 | 'zh_CN': () => import('./zh-CN.json'), 27 | 'zh_HK': () => import('./zh-HK.json'), 28 | 'en': () => import('./en.json'), 29 | 'es': () => import('./es-ES.json'), 30 | 'ja': () => import('./ja-JP.json'), 31 | 'ru': () => import('./ru.json'), 32 | }; 33 | 34 | const DEFAULT_LANGUAGE = 'en'; 35 | 36 | interface Props { 37 | AppData?: AppDataType; 38 | setData: (update: Partial<AppDataType>) => void; 39 | } 40 | 41 | const Language = ({ AppData, setData }: Props) => { 42 | // 获取系统语言 43 | const getSystemLanguage = (): string => { 44 | try { 45 | const systemLang = navigator.language; 46 | 47 | // 直接检查是否在支持的语言列表中 48 | if (SUPPORTED_LANGUAGES.includes(systemLang)) { 49 | return systemLang; 50 | } 51 | 52 | // 尝试提取语言代码(如从 "zh-CN" 中提取 "zh") 53 | const langCode = systemLang.split('-')[0]; 54 | if (SUPPORTED_LANGUAGES.includes(langCode)) { 55 | return langCode; 56 | } 57 | 58 | // 尝试将分隔符从 "-" 转换为 "_" 59 | const normalizedLang = systemLang.replace('-', '_'); 60 | if (SUPPORTED_LANGUAGES.includes(normalizedLang)) { 61 | return normalizedLang; 62 | } 63 | 64 | return DEFAULT_LANGUAGE; 65 | } catch (error) { 66 | console.warn('Failed to detect system language, using default:', error); 67 | return DEFAULT_LANGUAGE; 68 | } 69 | }; 70 | 71 | // 使用状态来跟踪当前语言,确保在检测到系统语言后立即更新 72 | const [currentLang, setCurrentLang] = useState<string>(AppData?.language || getSystemLanguage()); 73 | const [locale, setLocale] = useState<any>(null); 74 | const [loading, setLoading] = useState(false); 75 | const [initialized, setInitialized] = useState(false); 76 | 77 | // 初始化语言设置 78 | useEffect(() => { 79 | // 如果 AppData 中没有语言设置,设置系统语言 80 | if (!AppData?.language && !initialized) { 81 | const systemLang = getSystemLanguage(); 82 | setData({ language: systemLang }); 83 | setCurrentLang(systemLang); 84 | setInitialized(true); 85 | } else if (AppData?.language && AppData.language !== currentLang) { 86 | // 如果 AppData 中的语言与当前状态不同,更新状态 87 | setCurrentLang(AppData.language); 88 | } 89 | }, [AppData?.language, initialized, setData, currentLang]); 90 | 91 | // 异步加载语言包 92 | useEffect(() => { 93 | const loadLocale = async () => { 94 | setLoading(true); 95 | try { 96 | const loader = localeLoaders[currentLang] || localeLoaders[DEFAULT_LANGUAGE]; 97 | const mod = await loader(); 98 | setLocale(mod.default); 99 | } catch (error) { 100 | console.error(`Failed to load locale for ${currentLang}:`, error); 101 | // 回退到默认语言 102 | try { 103 | const fallbackLoader = localeLoaders[DEFAULT_LANGUAGE]; 104 | const mod = await fallbackLoader(); 105 | setLocale(mod.default); 106 | } catch (fallbackError) { 107 | console.error('Failed to load fallback locale:', fallbackError); 108 | setLocale({}); // 设置空对象避免崩溃 109 | } 110 | } finally { 111 | setLoading(false); 112 | } 113 | }; 114 | 115 | if (currentLang) { 116 | loadLocale(); 117 | } 118 | }, [currentLang]); 119 | 120 | // 获取当前语言标签 121 | const currentLabel = languageItems.find(item => item.key === currentLang)?.label 122 | || languageItems.find(item => item.key === DEFAULT_LANGUAGE)?.label 123 | || 'English'; 124 | 125 | return { 126 | Language: ( 127 | <Dropdown 128 | menu={{ 129 | items: languageItems as ItemType[], 130 | onClick: ({ key }) => { 131 | setData({ language: key }); 132 | setCurrentLang(key); 133 | }, 134 | disabled: loading 135 | }} 136 | placement="bottom" 137 | arrow 138 | disabled={loading} 139 | > 140 | <Button 141 | color="default" 142 | variant="filled" 143 | loading={loading} 144 | > 145 | {loading ? 'Loading...' : currentLabel} 146 | </Button> 147 | </Dropdown> 148 | ), 149 | locale, 150 | currentLang, 151 | loading 152 | }; 153 | }; 154 | 155 | export default Language; -------------------------------------------------------------------------------- /src/assets/StoreLogo.svg: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | <svg width="1em" height="1em" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 22 22"> 3 | <defs> 4 | <clipPath id="uuid-83a4ed6a-e66a-49ec-be37-9105910b188b"> 5 | <rect width="22" height="22" style="fill: none;"/> 6 | </clipPath> 7 | <clipPath id="uuid-afed7916-4029-41b2-9859-9235c4e3c051"> 8 | <rect width="22" height="22" style="fill: none;"/> 9 | </clipPath> 10 | <linearGradient id="uuid-f4b98309-e6c0-4884-84b6-2b36e8f4ee5d" data-name="未命名的渐变" x1="9.37" y1="25.57" x2="13.46" y2="6.36" gradientTransform="translate(0 28.67) scale(1 -1)" gradientUnits="userSpaceOnUse"> 11 | <stop offset="0" stop-color="#0669bc"/> 12 | <stop offset="1" stop-color="#243a5f"/> 13 | </linearGradient> 14 | <linearGradient id="uuid-d2ebc98f-9299-4c65-bfa6-169afce6da18" data-name="未命名的渐变 2" x1="9.37" y1="25.57" x2="13.46" y2="6.36" gradientTransform="translate(0 28.67) scale(1 -1)" gradientUnits="userSpaceOnUse"> 15 | <stop offset="0" stop-color="#0078d4"/> 16 | <stop offset="1" stop-color="#114a8b"/> 17 | </linearGradient> 18 | <mask id="uuid-87682041-f37f-4bb1-bb12-65f4ac600f4e" data-name="mask" x=".34" y="0" width="21.31" height="21.31" maskUnits="userSpaceOnUse"> 19 | <g id="uuid-0c505ff4-bb66-483e-a819-e0599b72f559" data-name="mask0 1049 9133"> 20 | <path d="M.34,3.67c0-.51.41-.92.92-.92h19.48c.51,0,.92.41.92.92v13.86c0,2.09-1.69,3.78-3.78,3.78H4.12c-2.09,0-3.78-1.69-3.78-3.78V3.67Z" style="fill: url(#uuid-d2ebc98f-9299-4c65-bfa6-169afce6da18);"/> 21 | </g> 22 | </mask> 23 | <linearGradient id="uuid-3df5e6b4-4eb7-4314-8559-f5557f322c2f" data-name="未命名的渐变 3" x1="10.93" y1="27.97" x2="11.13" y2="25.01" gradientTransform="translate(0 28.67) scale(1 -1)" gradientUnits="userSpaceOnUse"> 24 | <stop offset="0" stop-color="#28afea"/> 25 | <stop offset="1" stop-color="#0078d4"/> 26 | </linearGradient> 27 | <linearGradient id="uuid-6a9da33a-72ab-4b64-a910-f399e054d220" data-name="未命名的渐变 4" x1="10.89" y1="27.97" x2="11.08" y2="25.01" gradientTransform="translate(0 28.67) scale(1 -1)" gradientUnits="userSpaceOnUse"> 28 | <stop offset="0" stop-color="#30daff"/> 29 | <stop offset="1" stop-color="#0094d4"/> 30 | </linearGradient> 31 | <linearGradient id="uuid-91124716-d4d4-40a9-898c-c24f73e91ff2" data-name="未命名的渐变 5" x1="11" y1="28.67" x2="11" y2="27.29" gradientTransform="translate(0 28.67) scale(1 -1)" gradientUnits="userSpaceOnUse"> 32 | <stop offset="0" stop-color="#22bcff"/> 33 | <stop offset="1" stop-color="#0088f0"/> 34 | </linearGradient> 35 | <linearGradient id="uuid-729fb690-83b0-45dd-aaa8-6de45e69ad48" data-name="未命名的渐变 6" x1="11" y1="28.67" x2="11" y2="27.29" gradientTransform="translate(0 28.67) scale(1 -1)" gradientUnits="userSpaceOnUse"> 36 | <stop offset="0" stop-color="#28afea"/> 37 | <stop offset="1" stop-color="#3ccbf4"/> 38 | </linearGradient> 39 | <mask id="uuid-afbad4c1-4ef6-46c7-a503-34586c3c3909" data-name="mask-1" x="4.55" y="0" width="10.92" height="4.13" maskUnits="userSpaceOnUse"> 40 | <g id="uuid-b540a222-f319-4ac2-a461-41e1f1cdad42" data-name="mask1 1049 9133"> 41 | <rect x="6.53" width="8.94" height="1.38" style="fill: url(#uuid-729fb690-83b0-45dd-aaa8-6de45e69ad48);"/> 42 | </g> 43 | </mask> 44 | </defs> 45 | <g id="uuid-97e00b4a-314d-419d-b7fb-904f8c4dc090" data-name="图层 1" width="1em" 46 | height="1em"> 47 | <g style="clip-path: url(#uuid-83a4ed6a-e66a-49ec-be37-9105910b188b);"> 48 | <g style="clip-path: url(#uuid-afed7916-4029-41b2-9859-9235c4e3c051);"> 49 | <g> 50 | <path d="M.34,3.67c0-.51.41-.92.92-.92h19.48c.51,0,.92.41.92.92v13.86c0,2.09-1.69,3.78-3.78,3.78H4.12c-2.09,0-3.78-1.69-3.78-3.78V3.67Z" style="fill: url(#uuid-f4b98309-e6c0-4884-84b6-2b36e8f4ee5d);"/> 51 | <g style="mask: url(#uuid-87682041-f37f-4bb1-bb12-65f4ac600f4e);"> 52 | <path d="M5.84,3.44V.69h10.31v2.75" style="fill: none; stroke: url(#uuid-3df5e6b4-4eb7-4314-8559-f5557f322c2f); stroke-linecap: round; stroke-width: 1.38px;"/> 53 | </g> 54 | <g> 55 | <path d="M10.66,7.56h-4.12v4.12h4.12v-4.12Z" style="fill: #f25022;"/> 56 | <path d="M15.47,7.56h-4.12v4.12h4.12v-4.12Z" style="fill: #7fba00;"/> 57 | <path d="M15.47,12.38h-4.12v4.13h4.12v-4.13Z" style="fill: #ffb900;"/> 58 | <path d="M10.66,12.38h-4.12v4.13h4.12v-4.13Z" style="fill: #00a4ef;"/> 59 | </g> 60 | <path d="M5.84,3.44V1.38c0-.38.31-.69.69-.69h8.94c.38,0,.69.31.69.69v2.06" style="fill: none; stroke: url(#uuid-6a9da33a-72ab-4b64-a910-f399e054d220); stroke-linecap: round; stroke-width: 1.38px;"/> 61 | <rect x="6.53" width="8.94" height="1.38" style="fill: url(#uuid-91124716-d4d4-40a9-898c-c24f73e91ff2);"/> 62 | <g style="mask: url(#uuid-afbad4c1-4ef6-46c7-a503-34586c3c3909);"> 63 | <rect x="4.55" y=".69" width="1.99" height="3.44" style="fill: #c4c4c4;"/> 64 | </g> 65 | </g> 66 | </g> 67 | </g> 68 | </g> 69 | </svg> -------------------------------------------------------------------------------- /src/TitleBar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { MoonOutlined, SunOutlined } from '@ant-design/icons'; 3 | import { Button, ButtonProps, Divider, Flex, Segmented, ThemeConfig, Typography } from 'antd'; 4 | import { AliasToken } from 'antd/es/theme/internal'; 5 | import Logo from "./assets/logo.svg?react"; 6 | import { Window } from '@tauri-apps/api/window'; // 引入 appWindow 7 | //import { motion } from 'framer-motion'; // 引入 framer-motion 8 | import { invoke } from "@tauri-apps/api/core"; 9 | import { restoreStateCurrent, StateFlags } from '@tauri-apps/plugin-window-state'; 10 | import Close from "./assets/closed.svg?react"; 11 | import Mins from './assets/min.svg?react'; 12 | import usePageTitle from './mod/PageTitle' 13 | import { useAsyncEffect, useRequest } from 'ahooks'; 14 | const { Text } = Typography; 15 | interface Props { 16 | config: AliasToken 17 | themeDack: boolean 18 | locale: any 19 | setSpinning: React.Dispatch<React.SetStateAction<boolean>> 20 | spinning: boolean 21 | Themeconfig: ThemeConfig 22 | } 23 | const appWindow = new Window('main'); 24 | let WinS = true 25 | 26 | if (WinS) { 27 | WinS = false 28 | restoreStateCurrent(StateFlags.ALL); 29 | } 30 | 31 | 32 | 33 | const TitleButton: ButtonProps[] = [ 34 | { 35 | icon: <Mins />, 36 | shape: "round", 37 | type: "text", 38 | onClick: e => { 39 | // @ts-ignore 40 | e.target?.blur() 41 | appWindow.minimize() 42 | } 43 | }, 44 | { 45 | color: "danger", 46 | shape: "round", 47 | icon: <Close />, 48 | onClick: e => { 49 | // @ts-ignore 50 | e.target?.blur() 51 | appWindow.close() 52 | } 53 | } 54 | ] 55 | const upWindowTitle = async (PageTitle: string) => { 56 | if (typeof PageTitle === "string") { 57 | await appWindow.setTitle(PageTitle) 58 | } 59 | } 60 | const App: React.FC<Props> = ({ config, Themeconfig, themeDack, locale, setSpinning, spinning }) => { 61 | const PageTitle = usePageTitle() 62 | const { run } = useRequest(upWindowTitle, { 63 | debounceWait: 1000, 64 | manual: true, 65 | }); 66 | useAsyncEffect(async () => { 67 | run(PageTitle) 68 | }, [PageTitle]) 69 | async function changeTheme() { 70 | try { 71 | setSpinning(true) 72 | await invoke('set_system_theme', { isLight: themeDack }); 73 | //setThemeDack(!themeDack) 74 | console.log('主题切换到:', themeDack); 75 | } catch (error) { 76 | console.error('Error changing theme:', error); 77 | } 78 | } 79 | //更新窗口标题 80 | document.title = locale?.Title 81 | 82 | return ( 83 | <Flex 84 | style={{ 85 | backgroundColor: Themeconfig.components?.Layout?.headerBg, 86 | borderColor: config.colorBorder 87 | }} 88 | className="drag-region" 89 | gap="small" 90 | justify='space-between' 91 | align='center' 92 | data-tauri-drag-region> 93 | <Flex align='center' gap={'small'}> 94 | <Logo className='logo' /> 95 | <Text 96 | strong 97 | style={{ 98 | margin: 0, 99 | maxWidth: 'calc(100vw - 222px)' 100 | }} 101 | ellipsis={true} 102 | > 103 | {locale?.Title} 104 | </Text> 105 | </Flex> 106 | <Flex align='center' gap={'small'}> 107 | <Segmented 108 | disabled={spinning} 109 | shape="round" 110 | size='small' 111 | block 112 | value={themeDack ? 'Moon' : 'Sun'} 113 | options={[ 114 | { value: 'Moon', icon: <MoonOutlined /> }, 115 | { value: 'Sun', icon: <SunOutlined /> }, 116 | ]} 117 | onChange={async () => { 118 | await changeTheme(); 119 | } 120 | } 121 | /> 122 | <Flex 123 | className='ant-segmented ant-segmented-shape-round ant-segmented-sm' 124 | align='center' 125 | style={{ 126 | display: 'flex', 127 | }} 128 | > 129 | {TitleButton.map((item, index) => ( 130 | <React.Fragment key={`fragment-${index}`}> 131 | {index > 0 ? 132 | <Divider 133 | style={{ marginInline: 2, marginBlock: 0 }} 134 | type='vertical' /> 135 | : null} 136 | <Button 137 | size='small' 138 | className='titlebar ant-segmented-item-label' 139 | variant="text" 140 | {...item} 141 | /> 142 | </React.Fragment> 143 | 144 | ))} 145 | </Flex> 146 | 147 | </Flex> 148 | </Flex> 149 | ); 150 | } 151 | 152 | export default App; 153 | -------------------------------------------------------------------------------- /src/mod/sociti.ts: -------------------------------------------------------------------------------- 1 | import { fetch as fetchHttp } from '@tauri-apps/plugin-http'; 2 | import * as pako from 'pako'; 3 | import { positionType } from '../Type'; 4 | type Props = (key: string, lang?: string) => any; 5 | 6 | 7 | 8 | export const GetHttp = async (url: string, RequestInit?: RequestInit) => { 9 | const RequestInits = RequestInit ? RequestInit : { 10 | method: 'GET', 11 | headers: { 'Content-Type': 'application/json' }, 12 | } 13 | const response = await fetchHttp(url, RequestInits); 14 | 15 | if (response.ok) { 16 | const contentType = response.headers.get('Content-Type') || ''; 17 | const contentEncoding = response.headers.get('Content-Encoding'); 18 | 19 | let data; 20 | if (contentType.includes('text/html')) { 21 | // 如果是 HTML,则直接返回文本内容 22 | 23 | data = await response.text(); 24 | } else if (contentEncoding && contentEncoding.includes('gzip')) { 25 | // 响应体是 Gzip 压缩的,需要解压 26 | const arrayBuffer = await response.arrayBuffer(); 27 | const decompressed = pako.ungzip(new Uint8Array(arrayBuffer), { to: 'string' }); 28 | data = JSON.parse(decompressed); // 解压后解析 JSON 29 | } else { 30 | // 默认情况下解析 JSON 31 | data = await response.json(); 32 | } 33 | return data; 34 | } 35 | 36 | return false; 37 | }; 38 | const Apikey = 'bdd98ec1d87747f3a2e8b1741a5af796' 39 | const Languages: Record<string, string> = { 40 | 'zh_HK': 'zh-hant' 41 | } 42 | 43 | const AppCiti: Props = async (name, lang) => { 44 | lang = lang || 'en_US' as string 45 | const langs = Languages[lang] || lang.split('_')[0] 46 | let getUrl = '' 47 | if (name) { 48 | getUrl = `https://geoapi.qweather.com/v2/city/lookup?location=${encodeURI(name)}&lang=${langs}` 49 | } else { 50 | const range = (lang === 'zh_HK' ? 'CN' : lang.split('_')[1]).toUpperCase().toLowerCase(); 51 | getUrl = `https://geoapi.qweather.com/v2/city/top?number=10&lang=${langs}&range=${range}` 52 | } 53 | const url = `${getUrl}&key=${Apikey}`; 54 | console.log(url); 55 | 56 | const data = await GetHttp(url) 57 | 58 | return data 59 | } 60 | 61 | 62 | // 假设 GetHttp(url: string) => Promise<string | null | undefined> 63 | // 假设 extractSunMoonData(html: string) => Promise<YourResultType> 64 | 65 | type SunriseOptions = { 66 | maxAttempts?: number; // 最多尝试次数(包含首次请求),默认 10 67 | baseDelayMs?: number; // 基础退避时间(ms),默认 500 68 | throwOnFailure?: boolean; // 全部失败时是否抛出异常,默认 false(返回 null) 69 | }; 70 | 71 | 72 | // 定位位置 73 | export async function getLocation() { 74 | const data = await GetHttp("http://demo.ip-api.com/json/?fields=66842623&lang=en") 75 | const backdata: positionType = { 76 | lat: data?.lat, 77 | lng: data?.lon, 78 | tzid: data?.timezone 79 | } 80 | return backdata 81 | } 82 | function convertTo24Hour(timeStr: string): string { 83 | // 创建日期对象并设置时间 84 | const [time, period] = timeStr.split(' '); 85 | const [hours, minutes, seconds] = time.split(':').map(Number); 86 | 87 | // 设置小时(处理 12 小时制) 88 | let hour24 = hours; 89 | if (period === 'PM' && hours !== 12) { 90 | hour24 = hours + 12; 91 | } else if (period === 'AM' && hours === 12) { 92 | hour24 = 0; 93 | } 94 | 95 | // 格式化为两位数 96 | const formattedHour = hour24.toString().padStart(2, '0'); 97 | const formattedMinutes = minutes.toString().padStart(2, '0'); 98 | const formattedSeconds = seconds.toString().padStart(2, '0'); 99 | 100 | return `${formattedHour}:${formattedMinutes}:${formattedSeconds}`; 101 | } 102 | 103 | //查询气象数据 104 | async function Sunrise( 105 | LAL?: positionType, 106 | options?: SunriseOptions 107 | ): Promise<any | null> { 108 | const { maxAttempts = 3, baseDelayMs = 6000, throwOnFailure = false } = options ?? {}; 109 | // 简单的帮助函数:等待 ms 毫秒 110 | const sleep = (ms: number) => new Promise<void>((res) => setTimeout(res, ms)); 111 | for (let attempt = 1; attempt <= maxAttempts; attempt++) { 112 | try { 113 | // 构造 URL 114 | const url = `https://api.sunrise-sunset.org/json?lat=${LAL?.lat}&lng=${LAL?.lng}&tzid=${LAL?.tzid}`; 115 | const data = await GetHttp(url); 116 | if (data) { 117 | // 如果解析也可能失败,捕获并在必要时重试 118 | try { 119 | 120 | return { 121 | rise: convertTo24Hour(data.results.sunrise), 122 | set: convertTo24Hour(data.results.sunset) 123 | }; 124 | } catch (parseErr) { 125 | console.error('Sunrise: Failed to parse data:', parseErr); 126 | // 解析失败:如果达到最大尝试次数则抛/返回,否则继续重试 127 | if (attempt === maxAttempts) { 128 | if (throwOnFailure) throw parseErr; 129 | return null; 130 | } 131 | // 否则继续到下一次尝试(走到下面的等待逻辑) 132 | } 133 | } else { 134 | // data falsy (网络/请求失败),继续重试 135 | if (attempt === maxAttempts) { 136 | break; 137 | } 138 | } 139 | } catch (err) { 140 | // GetHttp 本身抛错也会来到这里。最后一次若仍然失败则抛/返回。 141 | if (attempt === maxAttempts) { 142 | if (throwOnFailure) throw err; 143 | return null; 144 | } 145 | // 否则继续重试 146 | } 147 | 148 | // 等待:指数退避 + 小随机抖动 149 | const expo = Math.pow(2, attempt - 1); // 1,2,4,8... 150 | const jitter = Math.floor(Math.random() * 200); // 0-199 ms 随机抖动 151 | const waitMs = baseDelayMs * expo + jitter; 152 | await sleep(waitMs); 153 | } 154 | 155 | // 全部尝试完仍未成功 156 | if (throwOnFailure) { 157 | throw new Error(); 158 | } 159 | return null; 160 | } 161 | 162 | export { AppCiti, Sunrise }; -------------------------------------------------------------------------------- /src/mod/update.ts: -------------------------------------------------------------------------------- 1 | export interface UpdateType { 2 | releaseNotes: string; 3 | latestVersion: string; 4 | releaseUrl: string; 5 | } 6 | 7 | export function formatVersion(version: string): string { 8 | return version.replace(/^v/, "").trim(); 9 | } 10 | 11 | export function compareVersions(a: string, b: string): number { 12 | const aParts = a.split(".").map(Number); 13 | const bParts = b.split(".").map(Number); 14 | const len = Math.max(aParts.length, bParts.length); 15 | for (let i = 0; i < len; i++) { 16 | const aNum = aParts[i] || 0; 17 | const bNum = bParts[i] || 0; 18 | if (aNum !== bNum) { 19 | return aNum - bNum; 20 | } 21 | } 22 | return 0; 23 | } 24 | 25 | export function isNewerVersion(current: string, latest: string): boolean { 26 | return compareVersions(current, latest) < 0; 27 | } 28 | 29 | export function parseBetaVersion(version: string): { base: string; beta: number } | null { 30 | const parts = version.split("-beta."); 31 | if (parts.length !== 2) return null; 32 | return { base: parts[0], beta: parseInt(parts[1]) }; 33 | } 34 | 35 | export async function checkForUpdates(currentVersion: string): Promise<UpdateType | null> { 36 | const repo = "tuyangJs/Windows_AutoTheme"; 37 | const apiUrl = `https://api.github.com/repos/${repo}/releases?per_page=100`; 38 | try { 39 | const response = await fetch(apiUrl); 40 | if (!response.ok) { 41 | throw new Error(`GitHub API 请求失败: ${response.status}`); 42 | } 43 | const releases = await response.json(); 44 | 45 | let update: UpdateType | null = null; 46 | // 对于 beta 用户,提取基础版本 47 | const currentVerFormatted = currentVersion.trim(); 48 | const currentIsBeta = currentVerFormatted.includes("beta"); 49 | const currentBase = currentIsBeta ? currentVerFormatted.split("-")[0].trim() : currentVerFormatted; 50 | 51 | if (currentIsBeta) { 52 | // 1. 先检测同一基础版本下的新 beta 版本 53 | const currentBeta = parseBetaVersion(currentVerFormatted)?.beta || 0; 54 | const betaReleases = releases.filter((r: any) => 55 | r.prerelease && 56 | !r.tag_name.includes("next") && 57 | formatVersion(r.tag_name).startsWith(`${currentBase}-beta.`) 58 | ); 59 | // console.log("筛选出的 beta 版本:", betaReleases.map((r: any) => formatVersion(r.tag_name))); 60 | betaReleases.sort((a: any, b: any) => { 61 | const aBeta = parseBetaVersion(formatVersion(a.tag_name))?.beta || 0; 62 | const bBeta = parseBetaVersion(formatVersion(b.tag_name))?.beta || 0; 63 | return aBeta - bBeta; 64 | }); 65 | const latestBeta = betaReleases.find((r: any) => { 66 | const candidateBeta = parseBetaVersion(formatVersion(r.tag_name))?.beta || 0; 67 | return candidateBeta > currentBeta; 68 | }); 69 | if (latestBeta) { 70 | update = { 71 | latestVersion: formatVersion(latestBeta.tag_name), 72 | releaseNotes: latestBeta.body, 73 | releaseUrl: latestBeta.html_url, 74 | }; 75 | console.log(`检测到新测试版本: v${update.latestVersion}`); 76 | return update; 77 | } else { 78 | // 2. 如果没有新的 beta,回退检测正式版更新 79 | const officialReleases = releases.filter((r: any) => !r.prerelease && !r.draft); 80 | console.log("beta fallback - 正式版本:", officialReleases.map((r: any) => formatVersion(r.tag_name))); 81 | // 优先查找与基础版本完全匹配的正式版 82 | const matchingOfficial = officialReleases.find((r: any) => 83 | formatVersion(r.tag_name) === currentBase 84 | ); 85 | if (matchingOfficial) { 86 | // 如果正式版与基础版本数值上相等,但当前是 beta 版,则也认为有更新(从 beta 升级到稳定版) 87 | if (isNewerVersion(currentBase, formatVersion(matchingOfficial.tag_name)) || 88 | (currentIsBeta && compareVersions(currentBase, formatVersion(matchingOfficial.tag_name)) === 0)) { 89 | update = { 90 | latestVersion: formatVersion(matchingOfficial.tag_name), 91 | releaseNotes: matchingOfficial.body, 92 | releaseUrl: matchingOfficial.html_url, 93 | }; 94 | console.log(`beta fallback - 检测到正式版更新: v${update.latestVersion}`); 95 | return update; 96 | } 97 | } 98 | // 或者取最新的正式版 99 | if (officialReleases.length > 0) { 100 | officialReleases.sort((a: any, b: any) => compareVersions(formatVersion(a.tag_name), formatVersion(b.tag_name))); 101 | const latestOfficial = officialReleases[officialReleases.length - 1]; 102 | if (isNewerVersion(currentBase, formatVersion(latestOfficial.tag_name)) || 103 | (currentIsBeta && compareVersions(currentBase, formatVersion(latestOfficial.tag_name)) === 0)) { 104 | update = { 105 | latestVersion: formatVersion(latestOfficial.tag_name), 106 | releaseNotes: latestOfficial.body, 107 | releaseUrl: latestOfficial.html_url, 108 | }; 109 | console.log(`beta fallback - 检测到正式版更新: v${update.latestVersion}`); 110 | return update; 111 | } 112 | } 113 | return null; 114 | } 115 | } else { 116 | // 正式版逻辑,只检测正式发布(排除 draft 与 prerelease) 117 | const officialReleases = releases.filter((r: any) => !r.prerelease && !r.draft); 118 | // console.log("筛选出的正式版本:", officialReleases.map((r: any) => formatVersion(r.tag_name))); 119 | if (officialReleases.length === 0) return null; 120 | officialReleases.sort((a: any, b: any) => compareVersions(formatVersion(a.tag_name), formatVersion(b.tag_name))); 121 | const latestOfficial = officialReleases[officialReleases.length - 1]; 122 | console.log("当前版本:", currentVerFormatted, "最新正式版本:", formatVersion(latestOfficial.tag_name)); 123 | if (isNewerVersion(currentVerFormatted, formatVersion(latestOfficial.tag_name))) { 124 | update = { 125 | latestVersion: formatVersion(latestOfficial.tag_name), 126 | releaseNotes: latestOfficial.body, 127 | releaseUrl: latestOfficial.html_url, 128 | }; 129 | console.log(`检测到新正式版本: v${update.latestVersion}`); 130 | return update; 131 | } 132 | return null; 133 | } 134 | } catch (error) { 135 | console.error("检测更新失败:", error); 136 | return null; 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/mod/Mainoption.tsx: -------------------------------------------------------------------------------- 1 | import { enable, disable } from "@tauri-apps/plugin-autostart" 2 | import { Input, AutoComplete, AutoCompleteProps, TimePicker, Button, Flex, Segmented, Tooltip, Space } from "antd" 3 | import dayjs from "dayjs" 4 | import { AppCiti, getLocation, Sunrise } from "./sociti" 5 | import { positionType, TimesProps } from "../Type" 6 | import type { MessageInstance } from "antd/es/message/interface" 7 | import { useRequest, useUpdateEffect } from "ahooks" 8 | import { useEffect, useState } from "react" 9 | import { EnvironmentOutlined, LoadingOutlined, QuestionOutlined } from "@ant-design/icons" 10 | import { invoke } from "@tauri-apps/api/core" 11 | import { isWin11 } from "./ThemeConfig" 12 | import Deviation from "./Deviation" 13 | import { openUrl } from "@tauri-apps/plugin-opener" 14 | import { formatCityDisplayByHierarchy } from "./searchCiti" 15 | import ThemeSelector from "../com/ThemeSelector" 16 | import DataSave from "./DataSave" 17 | export interface mainsType { 18 | key: string; 19 | label: any; 20 | defaultvalue?: boolean | undefined; 21 | change: any; 22 | default?: string; 23 | setVal?: any; 24 | hide?: boolean; 25 | value?: string | boolean | undefined; 26 | loading?: boolean; 27 | 28 | } 29 | export type MainopType = (e: { 30 | messageApi: MessageInstance; 31 | locale: any; 32 | options: AutoCompleteProps['options'] 33 | getCity: (search?: string) => Promise<void> 34 | Language: JSX.Element 35 | themeDack: boolean 36 | }) => { 37 | openRc: () => Promise<void>, 38 | mains: mainsType[], 39 | CitiInit: (citiids?: positionType) => Promise<void> 40 | } 41 | 42 | 43 | const format = 'HH:mm'; 44 | const { RangePicker } = TimePicker; 45 | const isWin64App = await invoke<boolean>('is_running_in_msix'); 46 | const Mainoption: MainopType = ({ 47 | messageApi, 48 | locale, 49 | options, 50 | getCity, 51 | Language, 52 | themeDack 53 | }) => { 54 | const { setData, AppData } = DataSave() 55 | const [rcOpenLoad, setRcOpenLoad] = useState(false) 56 | const [startOpenLoad, setStartOpenLoad] = useState(false) 57 | const [CitiLoad, setCitiLoad] = useState(false) 58 | const [Citiname, setCitiname] = useState(AppData?.city?.name) 59 | const [openThemeSelector, setOpenThemeSelector] = useState(false) 60 | const confirmCiti = (_e: any, err: any) => { //确认选择城市 61 | console.log(err); 62 | setData({ city: { position: err.position, name: err.value } }) 63 | setCitiname(err.value) 64 | } 65 | 66 | const { run } = useRequest(getCity, { 67 | debounceWait: 800, 68 | manual: true, 69 | }); 70 | const upTary = (e: string) => { //更新托盘数据 71 | invoke('update_tray_menu_item_title', { 72 | quit: locale?.quit, 73 | show: locale?.show, 74 | tooltip: e, 75 | switch: `${locale?.switch}${themeDack ? locale.light : locale.dark}` 76 | }) 77 | } 78 | const openRc = async () => { //处理日出日落数据 79 | setRcOpenLoad(true) 80 | if (AppData?.city?.position) { 81 | const data = await Sunrise(AppData?.city?.position) 82 | if (data?.rise && data?.set) { 83 | const rise = data.rise 84 | const sun = data.set 85 | setData({ rawTime: [rise, sun], rcrl: true }) 86 | setRcOpenLoad(false) 87 | } else { 88 | messageApi.error(locale.main?.TabsOptionAError) //获取日出日落数据失败 89 | .then(() => { 90 | setData({ rcrl: false }) 91 | setRcOpenLoad(false) 92 | }) 93 | } 94 | } else { 95 | CitiInit() 96 | } 97 | 98 | } 99 | 100 | const CitiInit = async (citiids?: positionType) => { 101 | setCitiLoad(true) 102 | if (AppData?.language) { //必须初始语言才会开始自动获取定位 103 | //const citiID = citiids ? { hid: citiids } : await Sunrise('', AppData?.language) 104 | let citiID: positionType | undefined = citiids 105 | if (!citiids) { 106 | citiID = await getLocation() 107 | if (!citiID) { 108 | messageApi.error(locale.main?.TabsOptionBError) //获取定位数据失败 109 | .then(() => { 110 | setData({ city: { name: '' } }) 111 | setCitiLoad(false) 112 | }) 113 | return 114 | } 115 | } 116 | if (citiID?.lng) { 117 | const Citiop = await AppCiti(`${citiID.lng},${citiID.lat}`, AppData?.language) 118 | const err = Citiop.location?.[0] 119 | console.log(err); 120 | 121 | const names = formatCityDisplayByHierarchy(err) 122 | setCitiname(names) 123 | setData({ city: { position: citiID, name: names }, rcrl: true }) 124 | } 125 | 126 | } 127 | setCitiLoad(false) 128 | } 129 | 130 | useEffect(() => { 131 | if (locale?.quit) { 132 | const tooltip = `${locale?.Title} - App \n${locale.Time}: ${AppData?.times?.[0]} - ${AppData?.times?.[1]}` 133 | upTary(tooltip) 134 | } 135 | 136 | }, [locale, AppData?.times, themeDack]) 137 | useUpdateEffect(() => { //只要首次运行时才会启动 138 | run() 139 | CitiInit(AppData?.city?.position) 140 | }, [AppData?.language]) 141 | const AutostartOpen = async (e: boolean) => { 142 | setStartOpenLoad(true) 143 | try { 144 | if (e) { 145 | await enable(); 146 | } else { 147 | disable(); 148 | } 149 | setData({ Autostart: e }) 150 | } catch (error) { 151 | messageApi.error(error as string) 152 | } 153 | setStartOpenLoad(false) 154 | } 155 | const handleTimeChange = (_e: any, dateStrings: [string, string]) => { //更改时间 156 | setData({ times: dateStrings }) 157 | } 158 | const startTime = dayjs(AppData?.times?.[0] || '08:08', 'HH:mm') 159 | const endTime = dayjs(AppData?.times?.[1] || '18:08', 'HH:mm') 160 | const Citidiv = ( //城市选择器 161 | <Flex gap={4} 162 | style={{ maxWidth: 320 }} 163 | > 164 | <Button type="text" 165 | disabled={!AppData?.rcrl || CitiLoad} 166 | color="default" 167 | //variant="filled" 168 | onClick={() => CitiInit()} 169 | icon={CitiLoad ? <LoadingOutlined /> : <EnvironmentOutlined />} 170 | /> 171 | <AutoComplete 172 | popupMatchSelectWidth={280} 173 | options={options} 174 | value={Citiname} 175 | onSelect={confirmCiti} 176 | onChange={run} 177 | disabled={!AppData?.rcrl || CitiLoad} 178 | > 179 | <Input 180 | disabled={CitiLoad} 181 | variant="filled" 182 | onChange={e => setCitiname(e.target.value)} 183 | placeholder={locale?.main?.citiPlaceholder} /> 184 | </AutoComplete> 185 | </Flex> 186 | ) 187 | const Times: React.FC<TimesProps> = ({ disabled }) => ( //渲染时间选择器 188 | <RangePicker 189 | variant="filled" 190 | disabled={disabled} 191 | style={{ width: 200 }} 192 | defaultValue={[startTime, endTime]} 193 | format={format} 194 | onChange={handleTimeChange} /> 195 | ); 196 | 197 | const mains: mainsType[] = [ // 全部选项数据 198 | { 199 | key: 'open', 200 | label: locale?.main?.open, 201 | defaultvalue: AppData?.open, 202 | change: (e: boolean) => { 203 | setData({ open: e }) 204 | } 205 | }, 206 | { 207 | key: "language", 208 | label: locale?.main?.language, 209 | change: Language 210 | }, 211 | 212 | { 213 | key: "rcrl", 214 | label: locale?.main?.TabsOptionA, 215 | value: AppData?.rcrl, 216 | loading: rcOpenLoad, 217 | change: (e: boolean) => { 218 | setData({ rcrl: e }) 219 | } 220 | }, 221 | { 222 | key: 'city', 223 | label: locale?.main?.citiTitle, 224 | hide: !AppData?.rcrl, 225 | change: Citidiv 226 | }, 227 | { 228 | key: 'deviation', 229 | label: locale?.main?.deviationTitle, 230 | hide: !AppData?.rcrl, 231 | change: <Deviation 232 | value={AppData?.deviation || 20} 233 | setVal={(e) => { 234 | setData({ deviation: e }); 235 | }} 236 | prompt={ 237 | <> 238 | {locale?.main?.deviationPrompt} 239 | <br /> 240 | { 241 | `${locale?.main?.TabsOptionB}: 242 | ${AppData?.rawTime[0]} - ${AppData?.rawTime[1]} 243 | ` 244 | } 245 | <br /> 246 | { 247 | ` ${locale?.main?.deviationTitle}: 248 | ${AppData?.times?.[0]} - ${AppData?.times?.[1]} ` 249 | } 250 | </> 251 | } 252 | /> 253 | }, 254 | { 255 | key: 'dark', 256 | label: locale?.main?.TabsOptionB, 257 | hide: AppData?.rcrl, 258 | change: <Times disabled={AppData?.rcrl} /> // 渲染时间选择器 259 | }, 260 | { 261 | key: "switchStyemMode", 262 | label: locale?.main?.switchStyemMode, 263 | change: <Space> 264 | <Tooltip 265 | title={locale?.main?.switchStyemModeTip} 266 | > 267 | <Button 268 | icon={<QuestionOutlined />} 269 | type="text" 270 | /> 271 | </Tooltip> 272 | <ThemeSelector 273 | locale={locale} 274 | isModalOpen={openThemeSelector} 275 | setIsModalOpen={setOpenThemeSelector} 276 | /> 277 | <Button onClick={() => setOpenThemeSelector(true)}>设置</Button> 278 | </Space> 279 | }, 280 | { 281 | key: "winBgEffect", 282 | label: locale?.main?.winBgEffect, 283 | change: <Segmented 284 | shape="round" 285 | value={AppData?.winBgEffect} 286 | onChange={e => setData({ winBgEffect: e }) 287 | } 288 | options={[ 289 | { value: 'Default', label: locale?.main?.Default }, 290 | { value: 'Mica', label: locale?.main?.Mica, disabled: !isWin11 }, 291 | { value: 'Acrylic', label: locale?.main?.Acrylic, disabled: !isWin11 }, 292 | ]} 293 | /> 294 | }, 295 | { 296 | key: 'Autostart', 297 | label: locale?.main?.Autostart, 298 | value: AppData?.Autostart, 299 | loading: startOpenLoad, 300 | change: isWin64App ? 301 | ( 302 | <Tooltip title={locale?.main?.AutostartTip}> 303 | <Button onClick={() => openUrl("ms-settings:startupapps")}>{locale?.main?.AutostartBtn}</Button> 304 | </Tooltip> 305 | ) : AutostartOpen, 306 | }, 307 | { 308 | key: "StartShow", 309 | label: locale?.main?.StartShow, 310 | defaultvalue: AppData?.StartShow, 311 | change: ((e: boolean) => setData({ StartShow: e })) 312 | } 313 | ]; 314 | return { openRc, mains, CitiInit } 315 | } 316 | 317 | export default Mainoption 318 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import TitleBar from "./TitleBar"; 3 | import dayjs from 'dayjs'; 4 | import "./App.css"; 5 | import { AutoCompleteProps, ConfigProvider, Flex, Layout, message, Spin } from "antd"; 6 | import { useUpdateEffect, useAsyncEffect } from "ahooks"; 7 | import LanguageApp from './language/index' 8 | import Docs from './doc' 9 | import { LoadingOutlined } from "@ant-design/icons"; 10 | import { ThemeFun } from './mod/ThemeConfig' 11 | import Mainoption from "./mod/Mainoption"; 12 | import DataSave from './mod/DataSave' 13 | import OpContent from './Content' 14 | import { CrontabTask, CrontabManager } from './mod/Crontab' 15 | import { searchResult } from "./mod/searchCiti"; 16 | import { invoke } from "@tauri-apps/api/core"; 17 | import { AppDataType } from "./Type"; 18 | import { getVersion } from '@tauri-apps/api/app'; 19 | import { isEnabled } from "@tauri-apps/plugin-autostart"; 20 | import { WindowBg } from "./mod/WindowCode"; 21 | import { listen } from "@tauri-apps/api/event"; 22 | import { adjustTime } from "./mod/adjustTime"; 23 | import { AnimatePresence, motion } from "framer-motion"; 24 | import { GetHttp } from "./mod/sociti"; 25 | import RatingPrompt from "./mod/RatingPrompt"; 26 | import { openStoreRating } from "./mod/openStoreRating"; 27 | import { Updates } from "./updates"; 28 | import { applyTheme } from "./mod/applyTheme"; 29 | GetHttp("https://dev.qweather.com/") 30 | async function fetchAppVersion() { 31 | try { 32 | const version = await getVersion(); 33 | return version 34 | // 在需要的地方使用版本号,例如显示在界面上 35 | } catch (error) { 36 | return '0.1.1' 37 | } 38 | } 39 | const version = await fetchAppVersion(); 40 | window.Webview.show() 41 | 42 | const { Content } = Layout; 43 | function App() { 44 | const { setData, AppData } = DataSave() 45 | const matchMedia = window.matchMedia('(prefers-color-scheme: light)'); 46 | const [themeDack, setThemeDack] = useState(!matchMedia.matches); 47 | const [options, setOptions] = useState<AutoCompleteProps['options']>([]); 48 | const [spinning, setSpinning] = useState(true) 49 | const [isWin64App, setIsWin64App] = useState(false) 50 | const [messageApi, contextHolder] = message.useMessage(); 51 | const { Language, locale } = LanguageApp({ AppData, setData }) 52 | //----EDN ---- Language 53 | useAsyncEffect(async () => { 54 | const inMsix = await invoke<boolean>('is_running_in_msix'); 55 | setIsWin64App(inMsix) 56 | }, []) 57 | 58 | useEffect(() => { 59 | setTimeout(async () => { 60 | const isVisible = await window.appWindow.isVisible() 61 | if (isVisible) { 62 | window.Webview.show() 63 | } else { 64 | window.Webview.hide() 65 | } 66 | }, 3000); 67 | }, []) 68 | 69 | //导入设置选项 70 | const { openRc, mains, CitiInit } = Mainoption({ 71 | messageApi, 72 | locale, 73 | options, 74 | getCity, 75 | Language, 76 | themeDack 77 | }) 78 | useEffect(() => { 79 | let isMounted = true; 80 | const setupListener = async () => { 81 | const unlisten = await listen("switch", async () => { 82 | if (!isMounted) return; 83 | console.log("switch dark", !themeDack); 84 | if (spinning) return; 85 | const isVisible = await window.appWindow.isVisible() 86 | setSpinning(true); 87 | setTimeout(async () => { 88 | await invoke('set_system_theme', { isLight: themeDack }); 89 | }, 10); 90 | if (!isVisible) { 91 | window.Webview.show() 92 | setTimeout(() => { 93 | window.appWindow.isVisible().then(async (_isVisible) => { 94 | console.log("isVisible", _isVisible); 95 | if (!_isVisible) { 96 | window.Webview.hide() 97 | } 98 | }) 99 | }, 600); 100 | } 101 | }); 102 | 103 | // 返回清理函数以移除事件监听器 104 | return () => { 105 | isMounted = false; 106 | if (unlisten) { 107 | try { 108 | unlisten(); 109 | } catch (cleanupError) { 110 | console.warn('Error while cleaning up listener:', cleanupError); 111 | } 112 | } 113 | }; 114 | }; 115 | const cleanupPromise = setupListener(); 116 | 117 | // 返回一个清理函数来处理异步操作的清理 118 | return () => { 119 | cleanupPromise.then(cleanup => cleanup()); 120 | }; 121 | }, [themeDack, spinning]); 122 | 123 | useUpdateEffect(() => { 124 | if (AppData?.open) { 125 | StartRady() 126 | } 127 | }, [AppData?.times, AppData?.open, AppData?.StyemThemeEnable]) 128 | useEffect(() => { 129 | if (AppData?.rawTime?.length === 2 && AppData?.rcrl) { 130 | const rise = adjustTime(AppData?.rawTime[0], AppData?.deviation) 131 | const sun = adjustTime(AppData?.rawTime[1], -AppData?.deviation) 132 | setData({ times: [rise, sun] }) 133 | } 134 | }, [AppData?.rawTime, AppData?.deviation, AppData?.rcrl]) 135 | useEffect(() => { //自动化获取日出日落数据 136 | if (AppData?.rcrl) { 137 | openRc() 138 | } 139 | }, [AppData?.city, AppData?.rcrl]) 140 | //设置窗口材料 141 | useEffect(() => { 142 | WindowBg(AppData as AppDataType, themeDack) 143 | }, [AppData?.winBgEffect, themeDack]) 144 | useEffect(() => { //初始化 -主题自适应 145 | const handleChange = function (this: any) { 146 | //appWindow.setTheme('') 147 | setThemeDack(!this.matches); 148 | setSpinning(false) 149 | }; 150 | matchMedia.addEventListener('change', handleChange); 151 | if (AppData?.open) { 152 | StartRady() 153 | } 154 | const isAutostart = async () => { 155 | setData({ Autostart: await isEnabled() }) 156 | } 157 | //检测开机启动 158 | isAutostart() 159 | // 清除事件监听器 160 | setTimeout(() => { 161 | setSpinning(false) 162 | }, 100); 163 | return () => { 164 | matchMedia.removeEventListener('change', handleChange); 165 | }; 166 | }, []); 167 | const StartRady = async () => { 168 | const presentTime = dayjs(); // 当前时间 169 | const sunriseTime = dayjs(AppData?.times?.[0], 'HH:mm'); // 日出时间 170 | const sunsetTime = dayjs(AppData?.times?.[1], 'HH:mm'); // 日落时间 171 | let isLight = false; 172 | let message = ''; 173 | if (presentTime.isAfter(sunriseTime) && presentTime.isBefore(sunsetTime)) { 174 | isLight = true; 175 | message = '现在是日出后,日落前'; 176 | } else if (presentTime.isBefore(sunriseTime)) { 177 | message = '现在是日出前'; 178 | } else { 179 | message = '现在是日落后'; 180 | } 181 | if (themeDack === isLight) { 182 | setSpinning(true); 183 | try { 184 | if (AppData.StyemThemeEnable) { 185 | setThemeDack(!isLight) 186 | return 187 | } 188 | await invoke('set_system_theme', { isLight }); 189 | console.log(message); 190 | } finally { 191 | setSpinning(false); 192 | } 193 | } 194 | }; 195 | 196 | 197 | useEffect(() => { //定时任务处理 198 | if (AppData?.open === false) { 199 | CrontabManager.clearAllTasks() 200 | return 201 | } 202 | if (AppData?.times?.[0] && AppData?.times?.[1]) { 203 | const onTaskExecute = async (time: string, data: { msg: string }) => { 204 | console.log(`执行任务: ${time}, 数据:`, data); 205 | switch (data.msg) { 206 | case 'TypeA': 207 | console.log(`执行任务: ${time}, 数据:`, data.msg); 208 | if (AppData.StyemThemeEnable) { 209 | setThemeDack(false) 210 | return 211 | } 212 | await invoke('set_system_theme', { isLight: true }); 213 | break; 214 | case 'TypeB': 215 | console.log(`执行任务: ${time}, 数据:`, data.msg); 216 | if (AppData.StyemThemeEnable) { 217 | setThemeDack(true) 218 | return 219 | } 220 | await invoke('set_system_theme', { isLight: false }); 221 | break; 222 | } 223 | console.log(CrontabManager.listTasks()); 224 | CitiInit(AppData?.city?.position) 225 | }; 226 | try { 227 | // 添加定时任务 228 | const task1: CrontabTask = { time: AppData?.times[0], data: { msg: 'TypeA' }, onExecute: onTaskExecute }; 229 | const task2: CrontabTask = { time: AppData?.times[1], data: { msg: 'TypeB' }, onExecute: onTaskExecute }; 230 | CrontabManager.addTask(task1); 231 | CrontabManager.addTask(task2); 232 | console.log('Tasks added successfully', CrontabManager.listTasks()); 233 | } catch (error) { 234 | console.error('Failed to add tasks:', error); 235 | } 236 | } 237 | return () => { 238 | CrontabManager.clearAllTasks() 239 | }; 240 | }, [AppData?.times, AppData?.open, AppData.StyemThemeEnable]) 241 | 242 | 243 | useUpdateEffect(() => { 244 | console.log(AppData?.open, AppData.StyemThemeEnable); 245 | 246 | if (!AppData?.open || !AppData.StyemThemeEnable) return 247 | 248 | if (AppData.StyemTheme) { 249 | applyTheme(AppData.StyemTheme[themeDack ? 1 : 0]) 250 | } 251 | }, [themeDack, AppData?.open, AppData.StyemTheme, AppData.StyemThemeEnable]) 252 | async function getCity(search?: string) { //搜索城市 253 | setOptions(await searchResult(search || '', AppData)) 254 | } 255 | 256 | const { Themeconfig, antdToken } = ThemeFun(themeDack, AppData?.winBgEffect) 257 | const animationVariants = (index: number) => ({ 258 | initial: { 259 | opacity: 0, 260 | x: 0, 261 | scale: 3, 262 | filter: "blur(5px)" 263 | }, 264 | animate: { 265 | opacity: 1, 266 | x: 0, 267 | scale: 1, 268 | filter: "blur(0px)", 269 | }, 270 | exit: { 271 | opacity: 0, 272 | x: 100, 273 | filter: "blur(5px)", 274 | transition: { 275 | duration: 0.36, 276 | delay: index * 0.36 // 第一个组件index为0,第二个为1,第三个为2,这样第二个组件会延迟0.36秒,第三个延迟0.72秒 277 | } 278 | }, 279 | transition: { 280 | duration: 0.26, 281 | delay: mains.length * 0.08 282 | }, 283 | }); 284 | 285 | // 统一的过渡配置 286 | const transitionConfig = { 287 | duration: 0.26, 288 | delay: mains.length * 0.08 289 | }; 290 | return ( 291 | <ConfigProvider 292 | theme={Themeconfig} 293 | > 294 | 295 | < Spin spinning={spinning} indicator={<LoadingOutlined spin style={{ fontSize: 48 }} />} > 296 | {contextHolder} 297 | <TitleBar 298 | spinning={spinning} 299 | locale={locale} 300 | setSpinning={setSpinning} 301 | config={antdToken} 302 | Themeconfig={Themeconfig} 303 | themeDack={themeDack} 304 | /> 305 | <Layout> 306 | <Content className="container"> 307 | <Flex gap={0} vertical > 308 | <AnimatePresence > 309 | <OpContent mains={mains} language={AppData?.language || 'en'} /> 310 | <motion.div 311 | key={`docs-${AppData?.language}`} 312 | variants={animationVariants(1)} 313 | initial="initial" 314 | animate="animate" 315 | exit="exit" 316 | transition={animationVariants(1).transition} 317 | layout 318 | > 319 | <Docs isWin64App={isWin64App} locale={locale} version={version} /> 320 | </motion.div> 321 | <motion.div 322 | key={`update-${AppData?.language}`} 323 | variants={animationVariants(1)} 324 | initial="initial" 325 | animate="animate" 326 | exit="exit" 327 | transition={transitionConfig} 328 | layout 329 | > 330 | {isWin64App ? <a 331 | onClick={() => openStoreRating("downloadsandupdates")} 332 | rel="noreferrer">{ 333 | locale?.upModal?.textB?.[1] 334 | }</a> : 335 | <Updates version={version} locale={locale} setData={setData} AppData={AppData} /> 336 | } 337 | 338 | </motion.div> 339 | </AnimatePresence> 340 | </Flex> 341 | { /* 评分组件 */} 342 | {isWin64App && <RatingPrompt locale={locale} />} 343 | </Content> 344 | </Layout> 345 | </Spin> 346 | 347 | 348 | </ConfigProvider > 349 | ); 350 | } 351 | 352 | export default App; 353 | -------------------------------------------------------------------------------- /src-tauri/src/lib.rs: -------------------------------------------------------------------------------- 1 | // 使用模块中的类型 2 | mod theme_apply; 3 | mod windows_themes; 4 | use std::ffi::OsStr; 5 | use std::os::windows::ffi::OsStrExt; 6 | use std::ptr; 7 | use std::sync::Mutex; 8 | use tauri::command; 9 | 10 | use crate::windows_themes::ThemeInfo; 11 | use tauri::window::{Effect, EffectState, EffectsBuilder}; 12 | use tauri::{ 13 | menu::{Menu, MenuItem, PredefinedMenuItem}, 14 | tray::{MouseButton, TrayIcon, TrayIconBuilder, TrayIconEvent}, 15 | }; 16 | use tauri::{AppHandle, Emitter, Manager, State}; 17 | use tauri_plugin_autostart::MacosLauncher; 18 | use tauri_plugin_log::{Target, TargetKind}; 19 | use theme_apply::ThemeApplier; 20 | use tokio::time::{sleep, Duration}; 21 | use winapi::shared::minwindef::{DWORD, HKEY}; 22 | use winapi::shared::winerror::ERROR_SUCCESS; 23 | use winapi::um::winreg::{RegOpenKeyExW, RegSetValueExW, HKEY_CURRENT_USER}; 24 | use winapi::{ 25 | ctypes::c_void, 26 | um::winuser::{SendMessageTimeoutW, HWND_BROADCAST, WM_SETTINGCHANGE}, 27 | }; 28 | struct AppState { 29 | tray: Mutex<Option<TrayIcon>>, 30 | } 31 | 32 | fn show_window(app: &AppHandle) { 33 | let windows = app.webview_windows(); 34 | //显示webview 35 | if let Some(window) = windows.values().next() { 36 | if let Err(e) = window.show() { 37 | eprintln!("无法显示窗口: {}", e); 38 | } 39 | if let Err(e) = window.unminimize() { 40 | eprintln!("无法解除窗口最小化: {}", e); 41 | } 42 | if let Err(e) = window.set_focus() { 43 | eprintln!("无法设置窗口焦点: {}", e); 44 | } 45 | } 46 | app.emit("show-app", ()).unwrap(); 47 | } 48 | 49 | async fn notify_system_theme_changed() { 50 | unsafe { 51 | let wparam = 0; 52 | let flags = 0x0002; 53 | let wide_str: Vec<u16> = "ImmersiveColorSet\0".encode_utf16().collect(); 54 | let lparam_wide = wide_str.as_ptr() as *const c_void; 55 | 56 | SendMessageTimeoutW( 57 | HWND_BROADCAST, 58 | WM_SETTINGCHANGE, 59 | wparam as usize, 60 | lparam_wide as isize, 61 | flags, 62 | 1000, 63 | ptr::null_mut(), 64 | ); 65 | } 66 | } 67 | 68 | fn set_registry_value(reg_path: &str, value_name: &str, value: u32) -> Result<(), String> { 69 | unsafe { 70 | let reg_path_wide: Vec<u16> = OsStr::new(reg_path).encode_wide().chain(Some(0)).collect(); 71 | let value_name_wide: Vec<u16> = OsStr::new(value_name) 72 | .encode_wide() 73 | .chain(Some(0)) 74 | .collect(); 75 | 76 | let mut hkey: HKEY = ptr::null_mut(); 77 | let status = RegOpenKeyExW( 78 | HKEY_CURRENT_USER, 79 | reg_path_wide.as_ptr(), 80 | 0, 81 | winapi::um::winnt::KEY_SET_VALUE, 82 | &mut hkey, 83 | ); 84 | 85 | if status != ERROR_SUCCESS as i32 { 86 | return Err(format!( 87 | "Failed to open registry key. Error code: {}", 88 | status 89 | )); 90 | } 91 | 92 | let result = RegSetValueExW( 93 | hkey, 94 | value_name_wide.as_ptr(), 95 | 0, 96 | winapi::um::winnt::REG_DWORD, 97 | &value as *const u32 as *const u8, 98 | std::mem::size_of::<u32>() as DWORD, 99 | ); 100 | 101 | if result != ERROR_SUCCESS as i32 { 102 | return Err(format!( 103 | "Failed to set registry value. Error code: {}", 104 | result 105 | )); 106 | } 107 | 108 | Ok(()) 109 | } 110 | } 111 | 112 | #[command] 113 | async fn set_system_theme(is_light: bool) { 114 | let theme_value = if is_light { 1 } else { 0 }; 115 | let reg_path = "Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; 116 | 117 | for reg_key in ["SystemUsesLightTheme", "AppsUseLightTheme"] { 118 | if let Err(e) = set_registry_value(reg_path, reg_key, theme_value) { 119 | eprintln!("Error setting registry value '{}': {}", reg_key, e); 120 | } 121 | } 122 | notify_system_theme_changed().await; 123 | tokio::spawn(async move { 124 | sleep(Duration::from_millis(155)).await; 125 | notify_system_theme_changed().await; 126 | }); 127 | } // 128 | fn send_event(app_handle: &AppHandle) { 129 | app_handle.emit("close-app", "quit").unwrap(); 130 | let app_handle_clone = app_handle.clone(); 131 | // 等待 3 秒 132 | tokio::spawn(async move { 133 | sleep(Duration::from_millis(3000)).await; 134 | app_handle_clone.exit(0); 135 | // 如果到达 3 秒后,程序还未结束,强制退出 136 | }); 137 | } 138 | fn create_system_tray(app: &AppHandle) -> tauri::Result<()> { 139 | let quit_i = MenuItem::with_id(app, "quit", "quit", true, None::<&str>)?; 140 | let show_i = MenuItem::with_id(app, "show", "show", true, None::<&str>)?; 141 | let menu = Menu::with_items(app, &[&show_i, &quit_i])?; 142 | let trays = TrayIconBuilder::new() 143 | .menu(&menu) 144 | .icon(app.default_window_icon().unwrap().clone()) 145 | .show_menu_on_left_click(false) 146 | .tooltip("Auto Theme Switching App") 147 | .on_menu_event(|tray, event| match event.id.as_ref() { 148 | "quit" => { 149 | println!("通知前端关闭应用..."); 150 | send_event(tray.app_handle()); 151 | } 152 | "show" => { 153 | show_window(&tray.app_handle()); 154 | } 155 | "switch" => { 156 | println!("切换系统主题..."); 157 | tray.app_handle().emit("switch", "switch").unwrap(); 158 | } 159 | _ => { 160 | println!("menu item {:?} not handled", event.id); 161 | } 162 | }) 163 | .on_tray_icon_event(|tray, event| match event { 164 | TrayIconEvent::DoubleClick { 165 | button: MouseButton::Left, 166 | .. 167 | } => { 168 | println!("托盘图标被左键双击"); 169 | show_window(&tray.app_handle()); 170 | } 171 | _ => {} 172 | }) 173 | .build(app)?; 174 | let state: State<AppState> = app.state(); 175 | let mut tray_lock = state.tray.lock().unwrap(); 176 | *tray_lock = Some(trays); 177 | Ok(()) 178 | } 179 | #[tauri::command] 180 | fn update_tray_menu_item_title( 181 | app: tauri::AppHandle, 182 | quit: String, 183 | show: String, 184 | tooltip: String, 185 | switch: String, 186 | ) { 187 | let app_handle = app.app_handle(); 188 | let state: State<AppState> = app.state(); 189 | // 获取托盘 190 | let mut tray_lock = state.tray.lock().unwrap(); 191 | let tray = match tray_lock.as_mut() { 192 | Some(tray) => tray, 193 | None => { 194 | eprintln!("Tray icon not found"); 195 | return; 196 | } 197 | }; 198 | 199 | // 创建菜单项 200 | let quit_i = match MenuItem::with_id(app_handle, "quit", quit, true, None::<&str>) { 201 | Ok(item) => item, 202 | Err(e) => { 203 | eprintln!("Failed to create menu item: {}", e); 204 | return; 205 | } 206 | }; 207 | // 创建菜单项 208 | let show_i = match MenuItem::with_id(app_handle, "show", show, true, None::<&str>) { 209 | Ok(item) => item, 210 | Err(e) => { 211 | eprintln!("Failed to create menu item: {}", e); 212 | return; 213 | } 214 | }; 215 | // 创建菜单项 216 | let switch = match MenuItem::with_id(app_handle, "switch", switch, true, None::<&str>) { 217 | Ok(item) => item, 218 | Err(e) => { 219 | eprintln!("Failed to create menu item: {}", e); 220 | return; 221 | } 222 | }; 223 | let separator = match PredefinedMenuItem::separator(app_handle) { 224 | Ok(item) => item, 225 | Err(e) => { 226 | eprintln!("Failed to create menu item: {}", e); 227 | return; 228 | } 229 | }; 230 | // 创建菜单 231 | let menu = match Menu::with_items(app_handle, &[&show_i, &switch, &separator, &quit_i]) { 232 | Ok(menu) => menu, 233 | Err(e) => { 234 | eprintln!("Failed to create menu: {}", e); 235 | return; 236 | } 237 | }; 238 | // 设置菜单 239 | if let Err(e) = tray.set_menu(Some(menu)) { 240 | eprintln!("Failed to set tray menu: {}", e); 241 | } else { 242 | println!("菜单项标题已更新"); 243 | } 244 | if let Err(e) = tray.set_tooltip(Some(tooltip)) { 245 | eprintln!("Failed to set tray menu: {}", e); 246 | } else { 247 | println!("托盘标题已更新"); 248 | } 249 | } 250 | #[tauri::command] 251 | fn is_running_in_msix() -> bool { 252 | if let Ok(exe_path) = std::env::current_exe() { 253 | if let Some(dir) = exe_path.parent() { 254 | let dir_str = dir.to_string_lossy().to_lowercase(); 255 | return dir_str.starts_with(r"c:\program files\windowsapps"); 256 | } 257 | } 258 | false 259 | } 260 | /// Read a single preview file and return as data URL (base64). 261 | 262 | #[tauri::command] 263 | async fn get_windows_themes() -> Vec<ThemeInfo> { 264 | tokio::task::spawn_blocking(|| windows_themes::get_all_themes()) 265 | .await 266 | .unwrap_or_default() 267 | } 268 | // Tauri 命令:应用主题(静默,不显示窗口) 269 | #[tauri::command] 270 | async fn apply_theme(theme_path: String) -> Result<(), String> { 271 | crate::ThemeApplier::apply_theme_by_path(&theme_path) 272 | } 273 | 274 | #[cfg_attr(mobile, tauri::mobile_entry_point)] 275 | pub fn run() { 276 | let rt = tokio::runtime::Runtime::new().unwrap(); 277 | rt.block_on(async { 278 | tauri::Builder::default() 279 | .plugin(tauri_plugin_fs::init()) 280 | .plugin(tauri_plugin_os::init()) 281 | .plugin(tauri_plugin_persisted_scope::init()) 282 | .plugin( 283 | tauri_plugin_log::Builder::new() 284 | .targets([ 285 | Target::new(TargetKind::Folder { 286 | path: std::path::PathBuf::from("/logs"), 287 | file_name: Some("app.log".to_string()), 288 | }), 289 | Target::new(TargetKind::Stdout), 290 | Target::new(TargetKind::LogDir { file_name: None }), 291 | Target::new(TargetKind::Webview), 292 | ]) 293 | .build(), 294 | ) 295 | .setup(|app| -> Result<(), Box<dyn std::error::Error>> { 296 | let app_handle = app.handle(); 297 | let main_window = app_handle 298 | .get_webview_window("main") 299 | .expect("Failed to get the main window"); 300 | main_window.hide().expect("Failed to hide the window"); 301 | main_window 302 | .set_always_on_top(false) 303 | .expect("Failed to set always on top"); 304 | main_window 305 | .set_effects( 306 | EffectsBuilder::new() 307 | .effect(Effect::Mica) 308 | .state(EffectState::FollowsWindowActiveState) 309 | .build(), 310 | ) 311 | .expect("Failed to set window effect"); 312 | main_window.set_shadow(true).expect("Failed to set shadow"); 313 | create_system_tray(&app_handle)?; 314 | Ok(()) 315 | }) 316 | .plugin(tauri_plugin_persisted_scope::init()) 317 | .plugin(tauri_plugin_window_state::Builder::new().build()) 318 | .plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| { 319 | show_window(app); 320 | })) 321 | .plugin(tauri_plugin_autostart::init( 322 | MacosLauncher::LaunchAgent, 323 | None, 324 | )) 325 | .plugin(tauri_plugin_http::init()) 326 | .plugin(tauri_plugin_opener::init()) 327 | .manage(AppState { 328 | tray: Mutex::new(None), 329 | }) 330 | .invoke_handler(tauri::generate_handler![ 331 | set_system_theme, 332 | update_tray_menu_item_title, 333 | is_running_in_msix, 334 | get_windows_themes, 335 | apply_theme, 336 | ]) 337 | .run(tauri::generate_context!()) 338 | .expect("error while running tauri application"); 339 | }); 340 | } 341 | -------------------------------------------------------------------------------- /src/com/ThemeSelector.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { Modal, Segmented, Row, Col, Card, message, Spin, Space, Typography, Switch, Tooltip, Tag, Flex } from 'antd'; 3 | import { invoke } from '@tauri-apps/api/core'; 4 | import { normalizeWindowsPath } from '../mod/utils/path'; 5 | import { useLocalImageUrl } from '../mod/utils/tauri-file'; 6 | import { motion, AnimatePresence, Variants, Transition, LayoutGroup } from 'framer-motion'; 7 | import { MoonOutlined, SunOutlined, WarningOutlined } from '@ant-design/icons'; 8 | import useAppData from '../mod/DataSave' 9 | export interface Theme { 10 | name: string; 11 | path: string; 12 | is_active: boolean; 13 | wallpaper?: string; 14 | system_mode?: string; 15 | app_mode?: string; 16 | displayPath?: string; 17 | displayWallpaper?: string; 18 | } 19 | 20 | export interface ThemeSelectorProps { 21 | isModalOpen: boolean; 22 | setIsModalOpen: (e: boolean) => void; 23 | locale: any 24 | } 25 | 26 | const { Text } = Typography; 27 | 28 | // 类型安全的 transition 29 | const springTransition: Transition = { 30 | type: 'spring', 31 | stiffness: 400, 32 | damping: 28, 33 | }; 34 | 35 | const containerVariants: Variants = { 36 | hidden: { opacity: 0, y: 6 }, 37 | show: { 38 | opacity: 1, 39 | y: 0, 40 | transition: { 41 | staggerChildren: 0.06, 42 | when: 'beforeChildren', 43 | }, 44 | }, 45 | }; 46 | 47 | const itemVariants: Variants = { 48 | hidden: { opacity: 0, y: 8, scale: 0.98 }, 49 | show: { opacity: 1, y: 0, scale: 1, transition: springTransition }, 50 | exit: { opacity: 0, y: -8, scale: 0.98, transition: { duration: 0.12 } }, 51 | }; 52 | const ThemeSelector: React.FC<ThemeSelectorProps> = ({ 53 | isModalOpen, 54 | setIsModalOpen, 55 | locale 56 | }) => { 57 | const { AppData, setData } = useAppData() 58 | const [currentMode, setCurrentMode] = useState<'light' | 'dark'>('light'); 59 | const [selectedLightTheme, setSelectedLightTheme] = useState<string>(AppData?.StyemTheme?.[0] || ''); 60 | const [selectedDarkTheme, setSelectedDarkTheme] = useState<string>(AppData?.StyemTheme?.[1] || ''); 61 | const [themes, setThemes] = useState<Theme[]>([]); 62 | const [loading, setLoading] = useState(false); 63 | useEffect(() => { 64 | setData({ 65 | StyemTheme: [selectedLightTheme, selectedDarkTheme] 66 | }) 67 | }, [selectedLightTheme, selectedDarkTheme]) 68 | useEffect(() => { 69 | const fetchThemes = async () => { 70 | if (!isModalOpen) return; 71 | setLoading(true); 72 | try { 73 | const themesData = (await invoke('get_windows_themes')) as Theme[]; 74 | 75 | const processed = themesData.map((t) => ({ 76 | ...t, 77 | displayPath: normalizeWindowsPath(t.path), 78 | displayWallpaper: normalizeWindowsPath(t.wallpaper), 79 | })); 80 | setThemes(processed); 81 | if (!selectedLightTheme) { 82 | const firstLight = processed.find((t) => t.system_mode?.toLowerCase() === 'light'); 83 | if (firstLight) setSelectedLightTheme(firstLight.path); 84 | } 85 | if (!selectedDarkTheme) { 86 | const firstDark = processed.find((t) => t.system_mode?.toLowerCase() === 'dark'); 87 | if (firstDark) setSelectedDarkTheme(firstDark.path); 88 | } 89 | } catch (err) { 90 | console.error('获取主题列表失败:', err); 91 | message.error('获取主题列表失败'); 92 | } finally { 93 | setTimeout(() => { 94 | setLoading(false); 95 | }, 688); 96 | } 97 | }; 98 | 99 | fetchThemes(); 100 | // eslint-disable-next-line react-hooks/exhaustive-deps 101 | }, [isModalOpen]); 102 | 103 | const lightThemes = themes.filter((t) => t.app_mode?.toLowerCase() === 'light'); 104 | const darkThemes = themes.filter((t) => t.app_mode?.toLowerCase() === 'dark'); 105 | const handleOk = (e: boolean) => { 106 | setData({ 107 | StyemThemeEnable: e 108 | }) 109 | }; 110 | 111 | const handleCancel = () => setIsModalOpen(false); 112 | 113 | const getCurrentThemes = () => (currentMode === 'light' ? lightThemes : darkThemes); 114 | const getCurrentSelected = () => (currentMode === 'light' ? selectedLightTheme : selectedDarkTheme); 115 | 116 | const handleCardClick = (themePath: string) => { 117 | if (currentMode === 'light') setSelectedLightTheme(themePath); 118 | else setSelectedDarkTheme(themePath); 119 | }; 120 | 121 | // 子组件:缩略图(确保 hook 在组件顶层稳定调用) 122 | const ThemeThumb: React.FC<{ wallpaperPath?: string; mode: 'light' | 'dark' }> = ({ wallpaperPath, mode }) => { 123 | const { src } = useLocalImageUrl(wallpaperPath); 124 | const fallback = mode === 'light' 125 | ? 'https://gw.alipayobjects.com/zos/bmw-prod/f601048d-61c2-44d0-bf57-ca1afe7fd92e.svg' 126 | : 'https://gw.alipayobjects.com/zos/bmw-prod/2c73c6a5-89e5-4d46-b243-317a848fc93f.svg'; 127 | 128 | return ( 129 | <img 130 | alt={wallpaperPath || 'thumb'} 131 | src={src || fallback} 132 | style={{ 133 | width: "100%", 134 | height: "100%", 135 | objectFit: 'cover', // 保持比例填充 136 | objectPosition: 'center', // 居中裁剪 137 | display: 'block', 138 | borderRadius: 0, 139 | }} 140 | draggable={false} 141 | /> 142 | ); 143 | }; 144 | 145 | const renderCard = (theme: Theme) => { 146 | const isSelected = getCurrentSelected() === theme.path; 147 | const label = locale?.themeName?.[theme.name] || theme.name; 148 | // 覆盖层样式:根据当前模式调整背景渐变与文字颜色 149 | const overlayStyle: React.CSSProperties = { 150 | position: 'absolute', 151 | left: 0, 152 | right: 0, 153 | bottom: 0, 154 | padding: '6px 8px', 155 | display: 'flex', 156 | justifyContent: 'center', 157 | alignItems: 'center', 158 | textAlign: 'center', 159 | fontSize: 12, 160 | lineHeight: '16px', 161 | pointerEvents: 'none', // 防止覆盖层阻挡点击 162 | borderBottomLeftRadius: 0, 163 | borderBottomRightRadius: 0, 164 | // 深色模式用深色渐变 + 白字;浅色模式用浅色半透明渐变 + 深色字 165 | background: currentMode === 'dark' 166 | ? 'linear-gradient(to top, rgba(0,0,0,0.65), rgba(0,0,0,0))' 167 | : 'linear-gradient(to top, rgba(255,255,255,0.8), rgba(255,255,255,0))', 168 | color: currentMode === 'dark' ? '#fff' : 'rgba(0,0,0,0.85)', 169 | }; 170 | 171 | const cover = ( 172 | <div style={{ 173 | position: 'relative', 174 | height: 92, 175 | overflow: 'hidden', 176 | borderTopLeftRadius: 8, 177 | borderTopRightRadius: 8 178 | }}> 179 | 180 | <Tooltip title={label} placement="bottom"> 181 | <ThemeThumb wallpaperPath={theme.displayWallpaper} mode={currentMode} /> 182 | <div style={overlayStyle}> 183 | <div style={{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }} title={label}> 184 | {label} 185 | </div> 186 | </div> 187 | </Tooltip> 188 | </div> 189 | ); 190 | 191 | return ( 192 | <motion.div 193 | key={theme.path} 194 | layout 195 | layoutId={`card-${theme.path}`} // 关键:为每张卡片提供 layoutId,支持在不同父容器间做平滑过渡 196 | variants={itemVariants} 197 | whileHover={{ scale: 1.03 }} 198 | whileTap={{ scale: 0.98 }} 199 | style={{ width: '100%', }} 200 | > 201 | <Card 202 | hoverable 203 | onClick={() => handleCardClick(theme.path)} 204 | style={{ 205 | width: '100%', 206 | marginBottom: 12, 207 | borderRadius: 8, 208 | cursor: 'pointer', 209 | border: isSelected ? '2px solid #1890ff' : undefined, 210 | boxShadow: isSelected ? '0 8px 20px rgba(65, 65, 65, 0.14)' : undefined, 211 | overflow: 'hidden', 212 | padding: 0, 213 | }} 214 | cover={cover} 215 | styles={{ 216 | body: { padding: 0 }, 217 | }} // body 保留小内边距,如果你想完全去掉正文内容可以设为 { padding: 0 } 218 | /> 219 | </motion.div> 220 | ); 221 | }; 222 | 223 | const currentLightTheme = lightThemes.find(t => t.path === selectedLightTheme); 224 | const currentDarkTheme = darkThemes.find(t => t.path === selectedDarkTheme); 225 | const footer = ( 226 | <> 227 | <Tag 228 | icon={<WarningOutlined />} 229 | color="warning" 230 | style={{ marginBottom: 8 }} 231 | > 232 | {locale?.main?.switchStyemModeOpenTip} 233 | </Tag> 234 | <Flex 235 | align="start" 236 | justify="space-between" 237 | style={{ width: "100%" }} 238 | > 239 | <Space direction="vertical" size={2} align='start'> 240 | <motion.div 241 | animate={{ 242 | width: "auto" // 让motion自己计算宽度 243 | }} 244 | transition={{ duration: 0.3 }} 245 | style={{ 246 | display: 'inline-block', 247 | }} 248 | > 249 | <Tag 250 | icon={<SunOutlined />} 251 | color="orange" 252 | > 253 | <AnimatePresence mode="wait"> 254 | <motion.span 255 | key={currentLightTheme?.name} 256 | initial={{ opacity: 0, y: 10 }} 257 | animate={{ opacity: 1, y: 0 }} 258 | exit={{ opacity: 0, y: -10 }} 259 | transition={{ duration: 0.2 }} 260 | > 261 | {currentLightTheme ? locale?.themeName?.[currentLightTheme.name] || currentLightTheme.name : '未选择'} 262 | </motion.span> 263 | </AnimatePresence> 264 | </Tag> 265 | </motion.div> 266 | 267 | <motion.div 268 | animate={{ 269 | width: "auto" 270 | }} 271 | transition={{ duration: 0.3 }} 272 | style={{ 273 | display: 'inline-block', 274 | }} 275 | > 276 | <Tag icon={<MoonOutlined />} color="cyan"> 277 | <AnimatePresence mode="wait"> 278 | <motion.span 279 | key={currentDarkTheme?.name} 280 | initial={{ opacity: 0, y: 10 }} 281 | animate={{ opacity: 1, y: 0 }} 282 | exit={{ opacity: 0, y: -10 }} 283 | transition={{ duration: 0.2 }} 284 | > 285 | {currentDarkTheme ? locale?.themeName?.[currentDarkTheme.name] || currentDarkTheme.name : '未选择'} 286 | </motion.span> 287 | </AnimatePresence> 288 | </Tag> 289 | </motion.div> 290 | </Space> 291 | <Space> 292 | <Text>{locale?.main?.open}</Text> 293 | <Switch 294 | checked={AppData?.StyemThemeEnable} 295 | onChange={handleOk} /> 296 | </Space> 297 | </Flex></> 298 | ) 299 | return ( 300 | <Modal 301 | title={ 302 | <div style={{ width: '100%', display: 'flex', justifyContent: 'center' }}> 303 | <Segmented 304 | value={currentMode} 305 | shape="round" 306 | onChange={(value) => setCurrentMode(value as 'light' | 'dark')} 307 | options={[ 308 | { value: 'light', label: `${locale?.ThemeLight} (${lightThemes.length})` }, 309 | { value: 'dark', label: `${locale?.ThemeDark} (${darkThemes.length})` }, 310 | ]} 311 | /> 312 | </div> 313 | } 314 | open={isModalOpen} 315 | onCancel={handleCancel} 316 | width={880} 317 | style={{ 318 | top: 28, 319 | padding: 0, 320 | }} 321 | footer={footer} 322 | confirmLoading={loading} 323 | className="theme-selector-modal" 324 | > 325 | {loading ? ( 326 | <Row justify="center" align="middle" style={{ padding: 0 }}> 327 | <Col> 328 | <Space direction="vertical" align="center"> 329 | <Spin /> 330 | <Text>Loading...</Text> 331 | </Space> 332 | </Col> 333 | </Row> 334 | ) : ( 335 | <> 336 | <LayoutGroup> 337 | <AnimatePresence initial={false} mode="popLayout"> 338 | <motion.div 339 | key={`theme-grid-${currentMode}-${getCurrentThemes().length}`} 340 | variants={containerVariants} 341 | initial="hidden" 342 | animate="show" 343 | exit="hidden" 344 | layout 345 | style={{ 346 | height: 342, overflowY: 'auto', overflowX: 'hidden' 347 | 348 | }} 349 | > 350 | <Row gutter={[16, 16]} wrap align="middle" justify="space-around"> 351 | {getCurrentThemes().map((theme) => ( 352 | <Col key={theme.path} span={12}> 353 | {renderCard(theme)} 354 | </Col> 355 | ))} 356 | </Row> 357 | </motion.div> 358 | </AnimatePresence> 359 | </LayoutGroup> 360 | 361 | </> 362 | )} 363 | </Modal> 364 | ); 365 | }; 366 | 367 | export default ThemeSelector; 368 | -------------------------------------------------------------------------------- /src-tauri/src/windows_themes/mod.rs: -------------------------------------------------------------------------------- 1 | use configparser::ini::Ini; 2 | use dirs; 3 | use encoding_rs; 4 | use serde::Serialize; 5 | use shellexpand; 6 | use std::fs; 7 | use std::path::{Path, PathBuf}; 8 | use winreg::enums::*; 9 | use winreg::RegKey; 10 | 11 | /// 主题信息结构体 12 | #[derive(Debug, Serialize, Clone)] 13 | pub struct ThemeInfo { 14 | pub name: String, 15 | pub path: String, 16 | pub is_active: bool, 17 | pub wallpaper: Option<String>, 18 | pub system_mode: Option<String>, // 新增:系统模式 19 | pub app_mode: Option<String>, // 新增:应用模式 20 | } 21 | 22 | /// 获取所有主题(系统 + 用户),壁纸路径转换为绝对路径 23 | pub fn get_all_themes() -> Vec<ThemeInfo> { 24 | let mut themes = vec![]; 25 | 26 | themes.extend(get_system_themes()); 27 | themes.extend(get_user_themes()); 28 | 29 | // 获取当前激活主题 30 | let current = get_current_theme().unwrap_or(None); 31 | 32 | // 标记激活主题 33 | for theme in &mut themes { 34 | if let Some(ref curr_path) = current { 35 | if theme.path.eq_ignore_ascii_case(curr_path) { 36 | theme.is_active = true; 37 | } 38 | } 39 | 40 | // 转换壁纸路径为绝对路径 41 | if let Some(ref wallpaper) = theme.wallpaper { 42 | let absolute_path = convert_to_absolute_path(wallpaper); 43 | theme.wallpaper = Some(absolute_path); 44 | } 45 | 46 | // 解析主题文件的模式信息(新增) 47 | let theme_path = Path::new(&theme.path); 48 | let (system_mode, app_mode) = read_theme_modes(theme_path); 49 | theme.system_mode = system_mode; 50 | theme.app_mode = app_mode; 51 | } 52 | 53 | themes 54 | } 55 | 56 | /// 新增:读取主题文件的模式信息 57 | fn read_theme_modes(theme_path: &Path) -> (Option<String>, Option<String>) { 58 | let content = if theme_path.to_string_lossy().contains("Users") { 59 | // 对用户主题使用专门的编码处理 60 | read_user_theme_with_correct_encoding(theme_path) 61 | } else { 62 | // 对系统主题使用常规处理 63 | read_file_with_fallback_encoding(theme_path) 64 | }; 65 | 66 | if content.is_empty() { 67 | return (None, None); 68 | } 69 | 70 | // 方法1: 使用 configparser 解析 71 | if let Ok((sys_mode, app_mode_val)) = parse_modes_with_configparser(&content) { 72 | if sys_mode.is_some() || app_mode_val.is_some() { 73 | return (sys_mode, app_mode_val); 74 | } 75 | } 76 | 77 | // 方法2: 使用手动解析 78 | let (sys_mode, app_mode_val) = parse_modes_manually(&content); 79 | 80 | (sys_mode, app_mode_val) 81 | } 82 | 83 | /// 新增:使用 configparser 解析模式信息 - 修复版本 84 | fn parse_modes_with_configparser(content: &str) -> Result<(Option<String>, Option<String>), Box<dyn std::error::Error>> { 85 | let mut config = Ini::new(); 86 | config.set_default_section(""); 87 | config.read(content.to_string())?; 88 | 89 | let mut system_mode = None; 90 | let mut app_mode = None; 91 | 92 | // 正确使用 configparser 的 get 方法,需要 section 和 key 两个参数 93 | if let Some(mode) = config.get("VisualStyles", "SystemMode") { 94 | system_mode = Some(mode); 95 | } 96 | 97 | if let Some(mode) = config.get("VisualStyles", "AppMode") { 98 | app_mode = Some(mode); 99 | } 100 | 101 | Ok((system_mode, app_mode)) 102 | } 103 | 104 | /// 新增:手动解析模式信息 105 | fn parse_modes_manually(content: &str) -> (Option<String>, Option<String>) { 106 | let mut system_mode = None; 107 | let mut app_mode = None; 108 | let mut in_visual_styles = false; 109 | 110 | for line in content.lines() { 111 | let line = line.trim(); 112 | 113 | // 检查节头 114 | if line.starts_with('[') && line.ends_with(']') { 115 | let section = &line[1..line.len()-1].trim(); 116 | in_visual_styles = section.eq_ignore_ascii_case("VisualStyles"); 117 | continue; 118 | } 119 | 120 | // 如果在 VisualStyles 节中,查找 SystemMode 和 AppMode 121 | if in_visual_styles { 122 | if line.to_lowercase().starts_with("systemmode") { 123 | if let Some(equal_pos) = line.find('=') { 124 | let mode = line[equal_pos+1..].trim().to_string(); 125 | if !mode.is_empty() { 126 | system_mode = Some(mode); 127 | } 128 | } 129 | } else if line.to_lowercase().starts_with("appmode") { 130 | if let Some(equal_pos) = line.find('=') { 131 | let mode = line[equal_pos+1..].trim().to_string(); 132 | if !mode.is_empty() { 133 | app_mode = Some(mode); 134 | } 135 | } 136 | } 137 | } 138 | 139 | // 如果两个都找到了,提前退出 140 | if system_mode.is_some() && app_mode.is_some() { 141 | break; 142 | } 143 | } 144 | 145 | (system_mode, app_mode) 146 | } 147 | 148 | // 以下是你原有的所有函数,保持不变 149 | 150 | /// 将包含环境变量的路径转换为绝对路径 151 | fn convert_to_absolute_path(path: &str) -> String { 152 | // 先展开环境变量 153 | let expanded = expand_env_vars_complete(path); 154 | 155 | // 尝试将路径转换为绝对路径 156 | let path_buf = PathBuf::from(&expanded); 157 | 158 | if path_buf.is_absolute() { 159 | // 如果已经是绝对路径,尝试规范化 160 | if let Ok(canonical) = std::fs::canonicalize(&path_buf) { 161 | canonical.to_string_lossy().to_string() 162 | } else { 163 | // 如果规范化失败,返回原始绝对路径 164 | expanded 165 | } 166 | } else { 167 | // 如果是相对路径,尝试基于当前工作目录转换为绝对路径 168 | if let Ok(cwd) = std::env::current_dir() { 169 | let absolute = cwd.join(&path_buf); 170 | if let Ok(canonical) = std::fs::canonicalize(&absolute) { 171 | canonical.to_string_lossy().to_string() 172 | } else { 173 | absolute.to_string_lossy().to_string() 174 | } 175 | } else { 176 | // 如果无法获取当前目录,返回展开后的路径 177 | expanded 178 | } 179 | } 180 | } 181 | 182 | /// 完全展开环境变量 183 | fn expand_env_vars_complete(s: &str) -> String { 184 | let mut result = s.to_string(); 185 | 186 | // 首先尝试使用 shellexpand 187 | if let Ok(expanded) = shellexpand::full(&result) { 188 | result = expanded.to_string(); 189 | } 190 | 191 | // 手动替换所有可能的环境变量,确保完全展开 192 | let env_vars = [ 193 | ("%SystemRoot%", std::env::var("SystemRoot").unwrap_or_else(|_| r"C:\Windows".to_string())), 194 | ("%windir%", std::env::var("windir").unwrap_or_else(|_| r"C:\Windows".to_string())), 195 | ("%USERPROFILE%", std::env::var("USERPROFILE").unwrap_or_default()), 196 | ("%HOMEPATH%", std::env::var("HOMEPATH").unwrap_or_default()), 197 | ("%HOMEDRIVE%", std::env::var("HOMEDRIVE").unwrap_or_else(|_| "C:".to_string())), 198 | ("%ProgramFiles%", std::env::var("ProgramFiles").unwrap_or_else(|_| r"C:\Program Files".to_string())), 199 | ("%ProgramFiles(x86)%", std::env::var("ProgramFiles(x86)").unwrap_or_else(|_| r"C:\Program Files (x86)".to_string())), 200 | ("%ProgramData%", std::env::var("ProgramData").unwrap_or_else(|_| r"C:\ProgramData".to_string())), 201 | ("%APPDATA%", std::env::var("APPDATA").unwrap_or_default()), 202 | ("%LOCALAPPDATA%", std::env::var("LOCALAPPDATA").unwrap_or_default()), 203 | ("%PUBLIC%", std::env::var("PUBLIC").unwrap_or_else(|_| r"C:\Users\Public".to_string())), 204 | ("%TEMP%", std::env::var("TEMP").unwrap_or_default()), 205 | ("%TMP%", std::env::var("TMP").unwrap_or_default()), 206 | ]; 207 | 208 | // 多次替换以确保嵌套的环境变量也被展开 209 | let mut changed; 210 | loop { 211 | changed = false; 212 | let mut new_result = result.clone(); 213 | 214 | for (var, value) in &env_vars { 215 | if new_result.contains(var) { 216 | new_result = new_result.replace(var, value); 217 | changed = true; 218 | } 219 | } 220 | 221 | if !changed || new_result == result { 222 | break; 223 | } 224 | result = new_result; 225 | } 226 | 227 | result 228 | } 229 | 230 | /// 获取系统主题 231 | pub fn get_system_themes() -> Vec<ThemeInfo> { 232 | let system_path = Path::new(r"C:\Windows\Resources\Themes"); 233 | read_themes_from_dir(system_path) 234 | } 235 | 236 | /// 获取用户自定义主题 237 | pub fn get_user_themes() -> Vec<ThemeInfo> { 238 | if let Some(user_path) = dirs::data_local_dir() { 239 | let theme_path = user_path.join("Microsoft").join("Windows").join("Themes"); 240 | read_themes_from_dir(&theme_path) 241 | } else { 242 | vec![] 243 | } 244 | } 245 | 246 | /// 从目录读取主题 247 | fn read_themes_from_dir(dir: &Path) -> Vec<ThemeInfo> { 248 | let mut result = vec![]; 249 | 250 | if let Ok(entries) = fs::read_dir(dir) { 251 | for entry in entries.flatten() { 252 | let path = entry.path(); 253 | if path.is_file() 254 | && path 255 | .extension() 256 | .map(|s| s.eq_ignore_ascii_case("theme")) 257 | .unwrap_or(false) 258 | { 259 | let name = path 260 | .file_stem() 261 | .unwrap_or_default() 262 | .to_string_lossy() 263 | .to_string(); 264 | let wallpaper = read_wallpaper_from_theme(&path); 265 | result.push(ThemeInfo { 266 | name, 267 | path: path.to_string_lossy().to_string(), 268 | is_active: false, 269 | wallpaper, 270 | system_mode: None, // 初始化为 None,后面会填充 271 | app_mode: None, // 初始化为 None,后面会填充 272 | }); 273 | } 274 | } 275 | } 276 | 277 | result 278 | } 279 | 280 | /// 获取当前激活主题(注册表) 281 | fn get_current_theme() -> Result<Option<String>, Box<dyn std::error::Error>> { 282 | let hkcu = RegKey::predef(HKEY_CURRENT_USER); 283 | let theme_key = hkcu.open_subkey("Software\\Microsoft\\Windows\\CurrentVersion\\Themes")?; 284 | let theme_mru: String = theme_key.get_value("CurrentTheme")?; 285 | let current_theme = theme_mru.split(';').next().map(|s| expand_env_vars(s)); 286 | Ok(current_theme) 287 | } 288 | 289 | /// 使用多种方法读取主题文件中的 Wallpaper 290 | fn read_wallpaper_from_theme(theme_path: &Path) -> Option<String> { 291 | // 检查是否是用户主题(路径包含 Users 或 用户名) 292 | let is_user_theme = theme_path.to_string_lossy().contains("Users"); 293 | 294 | let content = if is_user_theme { 295 | // 对用户主题使用专门的编码处理 296 | read_user_theme_with_correct_encoding(theme_path) 297 | } else { 298 | // 对系统主题使用常规处理 299 | read_file_with_fallback_encoding(theme_path) 300 | }; 301 | 302 | if content.is_empty() { 303 | return None; 304 | } 305 | 306 | // 检查是否是 aero.theme 并特别处理 307 | if theme_path.file_name().unwrap_or_default() == "aero.theme" { 308 | return parse_aero_theme_specially(&content); 309 | } 310 | 311 | // 方法1: 使用 configparser 解析 312 | if let Some(wallpaper) = parse_with_configparser(&content) { 313 | return Some(wallpaper); 314 | } 315 | 316 | // 方法2: 使用简化手动解析 317 | if let Some(wallpaper) = parse_wallpaper_simple(&content) { 318 | return Some(wallpaper); 319 | } 320 | 321 | // 方法3: 使用增强手动解析 322 | if let Some(wallpaper) = parse_wallpaper_enhanced(&content) { 323 | return Some(wallpaper); 324 | } 325 | 326 | None 327 | } 328 | 329 | /// 尝试多种编码读取文件 330 | fn read_file_with_fallback_encoding(theme_path: &Path) -> String { 331 | // 首先尝试 UTF-8 332 | if let Ok(content) = fs::read_to_string(theme_path) { 333 | return content; 334 | } 335 | 336 | // 然后尝试 UTF-16 LE 337 | if let Some(content) = read_utf16_le_file(theme_path) { 338 | return content; 339 | } 340 | 341 | // 尝试使用 encoding_rs 检测编码 342 | if let Ok(bytes) = fs::read(theme_path) { 343 | // 检测编码 344 | let (content, _, had_errors) = encoding_rs::UTF_16LE.decode(&bytes); 345 | if !had_errors { 346 | return content.into_owned(); 347 | } 348 | 349 | // 尝试 GBK 编码(中文Windows常用) 350 | let (content, _, had_errors) = encoding_rs::GBK.decode(&bytes); 351 | if !had_errors { 352 | return content.into_owned(); 353 | } 354 | 355 | // 最后尝试 Latin-1 356 | return bytes.iter().map(|&b| b as char).collect::<String>(); 357 | } 358 | 359 | String::new() 360 | } 361 | 362 | /// 专门处理用户主题文件的编码问题 363 | fn read_user_theme_with_correct_encoding(theme_path: &Path) -> String { 364 | if let Ok(bytes) = fs::read(theme_path) { 365 | // 优先尝试 UTF-16 LE 366 | if let Some(content) = read_utf16_le_file(theme_path) { 367 | return content; 368 | } 369 | 370 | // 尝试 GB18030 (中文Windows标准) 371 | let (content, _, had_errors) = encoding_rs::GB18030.decode(&bytes); 372 | if !had_errors { 373 | return content.into_owned(); 374 | } 375 | 376 | // 尝试 GBK 377 | let (content, _, had_errors) = encoding_rs::GBK.decode(&bytes); 378 | if !had_errors { 379 | return content.into_owned(); 380 | } 381 | 382 | // 尝试 UTF-8 383 | if let Ok(content) = String::from_utf8(bytes.clone()) { 384 | return content; 385 | } 386 | 387 | // 最后尝试系统本地编码 388 | let (content, _, _) = encoding_rs::WINDOWS_1252.decode(&bytes); 389 | return content.into_owned(); 390 | } 391 | 392 | String::new() 393 | } 394 | 395 | /// 读取 UTF-16 LE 编码的文件 396 | fn read_utf16_le_file(path: &Path) -> Option<String> { 397 | let bytes = fs::read(path).ok()?; 398 | 399 | if bytes.len() < 2 { 400 | return None; 401 | } 402 | 403 | // 检查 BOM 404 | let (start_index, is_utf16) = if bytes[0] == 0xFF && bytes[1] == 0xFE { 405 | (2, true) 406 | } else { 407 | (0, false) 408 | }; 409 | 410 | if is_utf16 || bytes.len() % 2 == 0 { 411 | let u16_chars: Vec<u16> = bytes[start_index..] 412 | .chunks_exact(2) 413 | .map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]])) 414 | .collect(); 415 | 416 | String::from_utf16(&u16_chars).ok() 417 | } else { 418 | None 419 | } 420 | } 421 | 422 | /// 使用 configparser 解析壁纸 - 修复版本 423 | fn parse_with_configparser(content: &str) -> Option<String> { 424 | let mut config = Ini::new(); 425 | config.set_default_section(""); 426 | 427 | if let Ok(_) = config.read(content.to_string()) { 428 | let section_names = [ 429 | "Control Panel\\Desktop", 430 | "Control Panel\\\\Desktop", 431 | "Control Panel\\Desktop.A", 432 | "Control Panel\\\\Desktop.A", 433 | "Desktop", 434 | ]; 435 | 436 | for section_name in §ion_names { 437 | if let Some(wallpaper) = config.get(section_name, "Wallpaper") { 438 | return Some(expand_env_vars(&wallpaper)); 439 | } 440 | } 441 | 442 | // 尝试无节名称 443 | if let Some(wallpaper) = config.get("", "Wallpaper") { 444 | return Some(expand_env_vars(&wallpaper)); 445 | } 446 | } 447 | 448 | None 449 | } 450 | 451 | /// 简化手动解析 - 直接查找 Wallpaper= 行 452 | fn parse_wallpaper_simple(content: &str) -> Option<String> { 453 | for line in content.lines() { 454 | let line = line.trim(); 455 | if line.to_lowercase().starts_with("wallpaper") { 456 | if let Some(equal_pos) = line.find('=') { 457 | let wallpaper_path = line[equal_pos+1..].trim(); 458 | if !wallpaper_path.is_empty() { 459 | return Some(expand_env_vars(wallpaper_path)); 460 | } 461 | } 462 | } 463 | } 464 | None 465 | } 466 | 467 | /// 增强手动解析 - 处理更多情况 468 | fn parse_wallpaper_enhanced(content: &str) -> Option<String> { 469 | let mut in_desktop_section = false; 470 | 471 | for line in content.lines() { 472 | let line = line.trim(); 473 | 474 | // 检查节头 475 | if line.starts_with('[') && line.ends_with(']') { 476 | let section = &line[1..line.len()-1].trim(); 477 | 478 | in_desktop_section = *section == "Control Panel\\Desktop" || 479 | *section == "Control Panel\\\\Desktop" || 480 | *section == "Control Panel\\Desktop.A" || 481 | *section == "Control Panel\\\\Desktop.A" || 482 | *section == "Desktop"; 483 | continue; 484 | } 485 | 486 | // 如果在正确的节中,查找 Wallpaper 键 487 | if in_desktop_section && line.to_lowercase().starts_with("wallpaper") { 488 | if let Some(equal_pos) = line.find('=') { 489 | let wallpaper_path = line[equal_pos+1..].trim(); 490 | if !wallpaper_path.is_empty() { 491 | return Some(expand_env_vars(wallpaper_path)); 492 | } 493 | } 494 | } 495 | } 496 | 497 | None 498 | } 499 | 500 | /// 特别处理 aero.theme 文件 501 | fn parse_aero_theme_specially(content: &str) -> Option<String> { 502 | let mut section = String::new(); 503 | 504 | for line in content.lines() { 505 | let line = line.trim(); 506 | 507 | if line.starts_with('[') && line.ends_with(']') { 508 | section = line[1..line.len()-1].to_string(); 509 | } 510 | 511 | // 在 Control Panel\Desktop 节中查找 Wallpaper 512 | if (section == "Control Panel\\Desktop" || section == "Control Panel\\\\Desktop") && 513 | line.to_lowercase().starts_with("wallpaper") { 514 | if let Some(equal_pos) = line.find('=') { 515 | let wallpaper = line[equal_pos+1..].trim(); 516 | if !wallpaper.is_empty() { 517 | return Some(expand_env_vars(wallpaper)); 518 | } 519 | } 520 | } 521 | } 522 | 523 | None 524 | } 525 | 526 | /// 展开环境变量(保持向后兼容) 527 | fn expand_env_vars(s: &str) -> String { 528 | expand_env_vars_complete(s) 529 | } --------------------------------------------------------------------------------