├── .github ├── changelog-configuration.json └── workflows │ ├── nightly.yml │ └── publish.yml ├── .gitignore ├── .scripts └── mirror-latest-json.js ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── README.zh-CN.md ├── biome.json ├── bun.lockb ├── bunfig.toml ├── daemon ├── Cargo.toml ├── build.rs └── src │ ├── color_mode.rs │ ├── config.rs │ ├── error.rs │ ├── lazy.rs │ ├── lib.rs │ ├── log.rs │ ├── main.rs │ ├── monitor │ ├── device_interface.rs │ ├── display_config.rs │ ├── error.rs │ ├── mod.rs │ ├── monitor_info.rs │ └── wallpaper_manager.rs │ ├── position │ ├── geolocation_access.rs │ ├── mod.rs │ ├── position_manager.rs │ ├── position_types.rs │ └── windows_api_helper.rs │ ├── registry.rs │ ├── solar.rs │ ├── theme │ ├── manager.rs │ ├── mod.rs │ ├── processor.rs │ └── validator.rs │ └── utils │ ├── mod.rs │ └── string.rs ├── images ├── home.avif └── settings.avif ├── index.html ├── package.json ├── src-tauri ├── .gitignore ├── Cargo.toml ├── build.rs ├── capabilities │ └── default.json ├── icons │ ├── 128x128.png │ ├── 128x128@2x.png │ ├── 32x32.png │ ├── Square107x107Logo.png │ ├── Square142x142Logo.png │ ├── Square150x150Logo.png │ ├── Square284x284Logo.png │ ├── Square30x30Logo.png │ ├── Square310x310Logo.png │ ├── Square44x44Logo.png │ ├── Square71x71Logo.png │ ├── Square89x89Logo.png │ ├── StoreLogo.png │ ├── android │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_round.png │ │ └── mipmap-xxxhdpi │ │ │ ├── ic_launcher.png │ │ │ ├── ic_launcher_foreground.png │ │ │ └── ic_launcher_round.png │ ├── icon.icns │ ├── icon.ico │ ├── icon.png │ └── ios │ │ ├── AppIcon-20x20@1x.png │ │ ├── AppIcon-20x20@2x-1.png │ │ ├── AppIcon-20x20@2x.png │ │ ├── AppIcon-20x20@3x.png │ │ ├── AppIcon-29x29@1x.png │ │ ├── AppIcon-29x29@2x-1.png │ │ ├── AppIcon-29x29@2x.png │ │ ├── AppIcon-29x29@3x.png │ │ ├── AppIcon-40x40@1x.png │ │ ├── AppIcon-40x40@2x-1.png │ │ ├── AppIcon-40x40@2x.png │ │ ├── AppIcon-40x40@3x.png │ │ ├── AppIcon-512@2x.png │ │ ├── AppIcon-60x60@2x.png │ │ ├── AppIcon-60x60@3x.png │ │ ├── AppIcon-76x76@1x.png │ │ ├── AppIcon-76x76@2x.png │ │ └── AppIcon-83.5x83.5@2x.png ├── src │ ├── auto_start.rs │ ├── cache │ │ ├── cache_manager.rs │ │ ├── cleanup_service.rs │ │ ├── error.rs │ │ ├── fs_service.rs │ │ ├── http_service.rs │ │ └── mod.rs │ ├── download │ │ ├── downloader.rs │ │ ├── error.rs │ │ ├── extractor.rs │ │ ├── file_manager.rs │ │ ├── http_service.rs │ │ ├── mod.rs │ │ └── task_manager.rs │ ├── error.rs │ ├── fs.rs │ ├── i18n │ │ ├── keys.rs │ │ ├── locales │ │ │ ├── en_us.rs │ │ │ ├── ja_jp.rs │ │ │ ├── mod.rs │ │ │ ├── zh_cn.rs │ │ │ ├── zh_hk.rs │ │ │ └── zh_tw.rs │ │ ├── mod.rs │ │ └── utils.rs │ ├── main.rs │ ├── monitor.rs │ ├── postion.rs │ ├── process_manager.rs │ ├── setup.rs │ ├── theme.rs │ └── window.rs ├── tauri.conf.json ├── tauri.debug.conf.json └── windows │ └── hooks.nsi ├── src ├── App.css.ts ├── App.tsx ├── commands │ ├── autostart.ts │ ├── config.ts │ ├── index.ts │ ├── monitor.ts │ ├── system.ts │ ├── theme.ts │ ├── translation.ts │ └── window.ts ├── components │ ├── DangerButton │ │ ├── DangerButton.css.ts │ │ ├── DangerButton.tsx │ │ └── index.ts │ ├── Dialog │ │ ├── Dialog.css.ts │ │ ├── Dialog.tsx │ │ └── index.ts │ ├── Download │ │ ├── Download.css.ts │ │ ├── Download.tsx │ │ └── index.ts │ ├── Flex │ │ ├── Flex.css.ts │ │ ├── Flex.tsx │ │ └── index.ts │ ├── Image.tsx │ ├── ImageCarousel │ │ ├── ImageCarousel.css.ts │ │ ├── ImageCarousel.tsx │ │ └── index.ts │ ├── NumericInput │ │ ├── NumericInput │ │ │ ├── NumericInput.css.ts │ │ │ ├── NumericInput.tsx │ │ │ ├── NumericInput.types.ts │ │ │ ├── index.ts │ │ │ └── useNumericInput.ts │ │ ├── NumericInputContainer │ │ │ ├── NumericInputContainer.css.ts │ │ │ ├── NumericInputContainer.tsx │ │ │ └── index.ts │ │ └── index.ts │ ├── Select │ │ ├── Select │ │ │ ├── Select.css.ts │ │ │ ├── Select.tsx │ │ │ ├── Select.types.ts │ │ │ └── index.ts │ │ ├── SelectContainer │ │ │ ├── SelectContainer.css.ts │ │ │ ├── SelectContainer.tsx │ │ │ ├── SelectContainer.types.ts │ │ │ └── index.ts │ │ └── index.ts │ ├── Settings │ │ ├── AutoDetectColorMode.tsx │ │ ├── AutoStart.tsx │ │ ├── CoordinateSource.tsx │ │ ├── Footer.tsx │ │ ├── GithubMirror.tsx │ │ ├── Interval.tsx │ │ ├── Item │ │ │ ├── SettingsItem.css.ts │ │ │ ├── SettingsItem.tsx │ │ │ └── index.ts │ │ ├── LockScreenWallpaperSwitch.tsx │ │ ├── ThemesDirectory.tsx │ │ └── index.tsx │ ├── Sidebar │ │ ├── Sidebar.css.ts │ │ ├── Sidebar.tsx │ │ ├── SidebarButtons.css.ts │ │ ├── SidebarButtons.tsx │ │ └── index.ts │ ├── ThemeActions.tsx │ ├── ThemeMenu │ │ ├── ThemeMenu.css.ts │ │ ├── ThemeMenu.tsx │ │ └── index.ts │ ├── ThemeShowcase.tsx │ └── Update │ │ ├── UpdateDialog.tsx │ │ └── index.tsx ├── contexts │ ├── ConfigContext.tsx │ ├── MonitorContext.tsx │ ├── SettingsContext.tsx │ ├── TaskContext.tsx │ ├── ThemeContext.tsx │ ├── TranslationsContext.tsx │ ├── UpdateContext.tsx │ └── index.tsx ├── hooks │ ├── __tests__ │ │ └── usePausableTimeout.test.ts │ ├── monitor │ │ ├── index.ts │ │ ├── useMonitorSelection.tsx │ │ └── useMonitorThemeSync.tsx │ ├── state │ │ ├── index.tsx │ │ ├── useConfigState.tsx │ │ └── useSettingsState.tsx │ ├── theme │ │ ├── useThemeApplication.tsx │ │ ├── useThemeSelection.tsx │ │ └── useThemeState.tsx │ ├── useAppInitialization.tsx │ ├── useColorMode.tsx │ ├── useDark.ts │ ├── useLocationPermission.tsx │ ├── usePausableTimeout.ts │ ├── useTaskManager.tsx │ └── useUpdateManager.tsx ├── index.css.ts ├── index.tsx ├── lazy.tsx ├── themes.ts ├── themes │ └── vars.css.ts ├── utils │ ├── array.ts │ ├── color.ts │ ├── i18n.ts │ └── proxy.ts └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json ├── types ├── config.d.ts ├── context.d.ts ├── i18n.d.ts ├── monitor.d.ts └── theme.d.ts └── vite.config.ts /.github/changelog-configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "categories": [ 3 | { 4 | "title": "## 🎉 Features", 5 | "labels": ["feature"] 6 | }, 7 | { 8 | "title": "## ⚙️ Refactors", 9 | "labels": ["refactor"] 10 | }, 11 | { 12 | "title": "## 🚀 Enhancements", 13 | "labels": ["enhancement"] 14 | }, 15 | { 16 | "title": "## 🐛 Fixes", 17 | "labels": ["fix", "bug"] 18 | }, 19 | { 20 | "title": "## 📦 Dependencies", 21 | "labels": ["dependencies"] 22 | }, 23 | { 24 | "title": "## 🎨 Styles", 25 | "labels": ["style"] 26 | }, 27 | { 28 | "title": "## 🛠 Breaking Changes", 29 | "labels": ["semver-major", "breaking-change"] 30 | } 31 | ], 32 | "pr_template": "- #{{TITLE}}", 33 | "base_branches": ["main"] 34 | } 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .DS_Store 4 | *.bak 5 | target 6 | -------------------------------------------------------------------------------- /.scripts/mirror-latest-json.js: -------------------------------------------------------------------------------- 1 | import fs from "node:fs"; 2 | import path from "node:path"; 3 | 4 | const latestJsonContent = fs.readFileSync(path.join("..", "latest.json"), { 5 | encoding: "utf8", 6 | }); 7 | 8 | const mirrors = [ 9 | { host: "gh-proxy.com", prefix: true }, 10 | { host: "kkgithub.com" }, 11 | ]; 12 | 13 | const GITHUB = "https://github.com/"; 14 | 15 | const mirrorContent = (mirror) => { 16 | if (mirror.prefix) { 17 | return latestJsonContent.replaceAll( 18 | GITHUB, 19 | `https://${mirror.host}/https://github.com/` 20 | ); 21 | } 22 | 23 | return latestJsonContent.replaceAll(GITHUB, `https://${mirror.host}/`); 24 | }; 25 | 26 | const newMirrorJSON = (mirror, filepath) => { 27 | const content = mirrorContent(mirror); 28 | fs.writeFileSync(filepath, content); 29 | }; 30 | 31 | const run = async () => { 32 | const currentDir = process.cwd(); 33 | const targetDir = path.join(currentDir, "mirrors"); 34 | 35 | if (!fs.existsSync(targetDir)) { 36 | fs.mkdirSync(targetDir); 37 | } 38 | 39 | mirrors.forEach((m, i) => 40 | newMirrorJSON(m, path.join(targetDir, `latest-mirror-${i + 1}.json`)) 41 | ); 42 | }; 43 | 44 | run(); 45 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = ["daemon", "src-tauri"] 4 | 5 | [workspace.package] 6 | authors = ["thep0y"] 7 | version = "0.1.24" 8 | # homepage = "" 9 | repository = "https://github.com/dwall-rs/dwall" 10 | license = "AGPL-3.0" 11 | edition = "2021" 12 | 13 | [workspace.dependencies] 14 | dirs = { version = "6", default-features = false } 15 | thiserror = { version = "2", default-features = false } 16 | serde = { version = "1", default-features = false, features = ["derive"] } 17 | serde_json = { version = "1", default-features = false } 18 | tracing = { version = "0", default-features = false, features = ["log"] } 19 | tracing-subscriber = { version = "0", default-features = false, features = [ 20 | "fmt", 21 | 'time', 22 | "local-time", 23 | 'env-filter', 24 | 'json', 25 | ] } 26 | tokio = { version = "1", default-features = false } 27 | windows = { version = "0.61", default-features = false } 28 | 29 | [profile.release] 30 | panic = "abort" 31 | codegen-units = 1 32 | lto = true 33 | incremental = false 34 | opt-level = "s" 35 | strip = true 36 | -------------------------------------------------------------------------------- /README.zh-CN.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |
4 | GitHub releases 5 | Windows 10 6 | Windows 11 7 |
8 | English | 简体中文 9 |

10 | 11 | # DWALL 12 | 13 | DWALL 是一款轻量级应用程序,旨在模拟 macOS 壁纸切换行为,通过基于太阳高度角和方位角动态更改 Windows 系统上的桌面背景。体验随着一天中太阳自然移动而无缝过渡的壁纸变化。 14 | 15 | ## 动机 16 | 17 | 面对现有的解决方案如 [WinDynamicDesktop](https://github.com/t1m0thyj/WinDynamicDesktop) 和 [AutoDarkMode](https://github.com/AutoDarkMode/Windows-Auto-Night-Mode),您可能会疑惑为何需要另一个工具。虽然这些应用程序提供类似的功能,但它们作为独立项目运行,没有整合计划。此外,它们的资源消耗可能高于此类任务的理想水平。 18 | 19 | 认识到这一差距,DWALL 被开发为一种高效的替代方案。它可以根据太阳位置自动切换明暗主题并更改壁纸,同时在运行期间保持最小的内存使用量。对于寻求资源占用更少选项的用户,DWALL 提供了一个极具吸引力的选择。 20 | 21 | ## 特性 22 | 23 | - **基于太阳的调度:** 自动根据太阳路径调整壁纸。 24 | - **低资源占用:** 针对性能进行优化,对系统资源的影响最小。 25 | - **无缝集成:** 轻松融入您的工作流程,没有侵入性通知或设置。 26 | 27 | ## 用前须知 28 | 29 | 本项目仍然处于开发阶段,可能存在一些问题。如果您在使用过程中遇到任何问题,请随时在 GitHub 页面上提出问题。 30 | 31 | > [!WARNING] 32 | > 本程序可能无法在精简版操作系统上运行,如果你使用的是精简版 Windows 操作系统,遇到错误时不需要提交 issue,安装官方 Windows 操作系统后再尝试运行本程序。 33 | 34 | ## 使用步骤 35 | 36 | 1. 在 [releases](https://github.com/dwall-rs/dwall/releases/latest) 中下载最新的 DWALL 可执行文件。 37 | 2. 运行 DWALL 可执行文件。 38 | 3. 允许 DWALL 访问您的位置信息,或者在设置页面手动设置您的位置。 39 | 4. 在侧边菜单中点击你想使用的壁纸,如果未下载,需要先点击“下载”按钮下载这套壁纸,如果下载完成则可以点击“应用”按钮使用这套壁纸。 40 | 5. 如果你使用多个显示器,可以在显示器选择器中选择你想单独配置的显示器,然后重复第 4 步的操作。 41 | 42 | ## 截图 43 | 44 | 以下是展示 DWALL 运行效果的两张截图: 45 | 46 | ![主页](images/home.avif) 47 | 48 | ![设置](images/settings.avif) 49 | 50 | ## 运行日志 51 | 52 | DWALL 的日志等级默认为`warning`,您可以通过设置环境变量`DWALL_LOG`来调整日志等级,如`DWALL_LOG=info`,需要注意的是,release 版本不会输出`info`以下的日志。 53 | 54 | ## 常见问题 55 | 56 | ### 1. 为什么设置和守护进程完全隔离? 57 | 58 | 最初的版本中,设置和守护进程是在同一个进程中运行的,同时支持通过任务栏托盘管理,但这会使得进程的内存占用较大(相比于其他同类型程序仍然要小很多),这不是我想要的结果,所以我设置和守护进程完全隔离,这样可以减少守护进程的内存占用,同时也使得进程的管理更加简单。 59 | 60 | 当然,这也带来了进程间通信的问题,目前的版本,设置进程(图形化程序)和守护进程没有实现进程间通信,二者交流的唯一途径是配置文件,这就会导致当守护进程异常退出时,设置进程无法及时获取守护进程的状态,这是一个需要解决的问题。 61 | 62 | ### 2. 壁纸下载失败。 63 | 64 | 壁纸资源保存在 github 中,但一些国家和地区因为网络管制无法正常访问 github,如果不设置 Github 镜像模板,就会导致壁纸下载失败。你需要通过搜索引擎自行搜索可用的 Github 镜像站或加速站。假设你搜索到了一个可用的 github 加速站`https://ghproxy.cc`,可在设置页面中如下配置 Github 镜像模板: 65 | 66 | ``` 67 | https://ghproxy.cc/https://github.com///releases/download// 68 | ``` 69 | 70 | 如果你搜索到到的是一个 github 镜镜站`https://kkgithub.com/`,则需要如下配置 Github 镜像模板: 71 | 72 | ``` 73 | https://kkgithub.com///releases/download// 74 | ``` 75 | 76 | 然后就可以正常下载壁纸了。 77 | 78 | ### 3. 为什么不支持自定义壁纸? 79 | 80 | 因为壁纸需要与太阳位置相关联,而大部分用户没有相关的天文学知识,无法将壁纸与太阳位置完美匹配,因此,我们提供一些已经与太阳位置完美关联的壁纸,用户可以根据自己的喜好选择。 81 | 82 | ### 4. 我能设计与太阳位置完美匹配的成套壁纸,如何添加到 DWALL 中? 83 | 84 | 你可以将制作好的壁纸上传到网盘当中,创建一个 issue 并说明设计思路,我们会在合适的时机将你的壁纸添加到 DWALL 中。 85 | 86 | ### 5. 缩略图为什么加载失败? 87 | 88 | 缩略图使用的是 Github 直链,部分地区对 Github 有网络管制,导致无法加载,你可以通过设置 Github 镜像模板来解决这个问题。 89 | 90 | ### 6. 为什么不使用托盘管理后台进程? 91 | 92 | 与第 1 个问题相同,托盘进程本身要比后台进程的内存占用还要大,考虑到这一点,我决定不使用托盘管理后台进程。 93 | 94 | --- 95 | 96 | 我们欢迎社区的贡献,以帮助改进 DWALL。如果您遇到任何问题或对新功能有建议,请随时在我们的 GitHub 页面上提出问题或提交拉取请求。 97 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.7.0/schema.json", 3 | "organizeImports": { 4 | "enabled": false 5 | }, 6 | "formatter": { 7 | "indentStyle": "space", 8 | "indentWidth": 2 9 | }, 10 | "linter": { 11 | "enabled": true, 12 | "rules": { 13 | "recommended": true, 14 | "correctness": { 15 | "useJsxKeyInIterable": "off" 16 | }, 17 | "a11y": { 18 | "useKeyWithClickEvents": "off" 19 | }, 20 | "style": { 21 | "noNonNullAssertion": "off" 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwall-rs/dwall/947d9e0e3a93b831b3c1b9c2354a652575c59b0e/bun.lockb -------------------------------------------------------------------------------- /bunfig.toml: -------------------------------------------------------------------------------- 1 | [install] 2 | registry = "https://registry.npmmirror.com" 3 | -------------------------------------------------------------------------------- /daemon/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dwall" 3 | description = "Dwall daemon" 4 | exclude = ["CHANGELOG.md", "/target"] 5 | readme = "README.md" 6 | version.workspace = true 7 | authors.workspace = true 8 | edition.workspace = true 9 | # homepage.workspace = true 10 | repository.workspace = true 11 | license.workspace = true 12 | 13 | [build-dependencies] 14 | winresource = { version = "0", optional = true } 15 | 16 | [dependencies] 17 | serde_valid = "1" 18 | time = { version = "0", default-features = false, features = [ 19 | 'macros', 20 | 'serde', 21 | ] } 22 | toml = { version = "0", default-features = false, features = [ 23 | "display", 24 | "parse", 25 | ] } 26 | tokio = { workspace = true, features = [ 27 | "sync", 28 | "macros", 29 | "time", 30 | "fs", 31 | "rt", 32 | "rt-multi-thread", 33 | ] } 34 | regex = { version = "1", default-features = false } 35 | dirs = { workspace = true } 36 | serde = { workspace = true } 37 | serde_json = { workspace = true } 38 | thiserror = { workspace = true } 39 | tracing = { workspace = true } 40 | tracing-subscriber = { workspace = true } 41 | windows = { workspace = true, default-features = false, features = [ 42 | "std", 43 | "Devices_Geolocation", 44 | "Win32_System_Registry", 45 | "System_UserProfile", 46 | "Storage_Streams", 47 | "Win32_UI", 48 | "Win32_UI_Shell", 49 | "Win32_UI_WindowsAndMessaging", 50 | "Win32_System_Com", 51 | "Win32_Graphics", 52 | "Win32_Graphics_Gdi", 53 | "Win32_Devices_Display", 54 | "Win32_Devices_DeviceAndDriverInstallation", 55 | ] } 56 | 57 | [features] 58 | default = [] 59 | log-color = ["tracing-subscriber/ansi"] 60 | build-script = ["winresource"] 61 | log-max-level-info = ["tracing/release_max_level_info"] 62 | -------------------------------------------------------------------------------- /daemon/build.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | fn main() -> io::Result<()> { 4 | #[cfg(feature = "build-script")] 5 | { 6 | use {std::env, winresource::WindowsResource}; 7 | 8 | if cfg!(not(feature = "log-max-level-info")) { 9 | println!("cargo:rustc-env=DWALL_LOG=debug"); 10 | }; 11 | 12 | if env::var_os("CARGO_CFG_WINDOWS").is_some() { 13 | WindowsResource::new() 14 | .set_icon("../src-tauri/icons/icon.ico") 15 | .set( 16 | "LegalCopyright", 17 | "Copyright (C) 2025 thep0y. All rights reserved.", 18 | ) 19 | .compile()?; 20 | } 21 | } 22 | Ok(()) 23 | } 24 | -------------------------------------------------------------------------------- /daemon/src/error.rs: -------------------------------------------------------------------------------- 1 | use crate::registry::RegistryError; 2 | 3 | /// Application result type, used for unified error handling 4 | pub type DwallResult = std::result::Result; 5 | 6 | /// Application global error type 7 | /// 8 | /// Contains all possible error types that may occur during application execution 9 | #[derive(Debug, thiserror::Error)] 10 | pub enum DwallError { 11 | /// Input/Output error 12 | #[error("IO operation failed: {0}")] 13 | Io(#[from] std::io::Error), 14 | 15 | /// Windows API error 16 | #[error("Windows system call failed: {0}")] 17 | Windows(#[from] windows::core::Error), 18 | 19 | /// Theme-related error 20 | #[error("Theme processing error: {0}")] 21 | Theme(#[from] crate::theme::ThemeError), 22 | 23 | /// JSON serialization/deserialization error 24 | #[error("JSON processing failed: {0}")] 25 | SerdeJson(#[from] serde_json::Error), 26 | 27 | /// Configuration-related error 28 | #[error("Configuration error: {0}")] 29 | Config(#[from] crate::config::ConfigError), 30 | 31 | /// Registry related error 32 | #[error("Registry error: {0}")] 33 | Registry(#[from] RegistryError), 34 | 35 | /// Null character error 36 | #[error("String contains null character: {0}")] 37 | NulError(#[from] std::ffi::NulError), 38 | 39 | /// Time offset error 40 | #[error("Unable to determine time offset: {0}")] 41 | TimeIndeterminateOffset(#[from] time::error::IndeterminateOffset), 42 | 43 | /// Monitor related error 44 | #[error("Monitor operation failed: {0}")] 45 | Monitor(#[from] crate::monitor::error::MonitorError), 46 | 47 | /// Position related error 48 | #[error("Position error: {0}")] 49 | Position(#[from] crate::position::PositionError), 50 | 51 | /// Geolocation access error 52 | #[error("Geolocation access error: {0}")] 53 | GeolocationAccess(#[from] crate::position::GeolocationAccessError), 54 | 55 | /// Timeout error 56 | #[error("Operation timed out: {0}")] 57 | Timeout(String), 58 | 59 | /// Timeout error 60 | #[error("Operation timed out: {0}")] 61 | MonitorWallpaperManagerError(#[from] crate::monitor::MonitorWallpaperManagerError), 62 | } 63 | -------------------------------------------------------------------------------- /daemon/src/lazy.rs: -------------------------------------------------------------------------------- 1 | use std::{fs, path::PathBuf, sync::LazyLock}; 2 | 3 | pub static DWALL_CONFIG_DIR: LazyLock = LazyLock::new(|| { 4 | let config_dir = dirs::config_dir().unwrap(); 5 | 6 | let app_config_dir = config_dir.join("dwall"); 7 | 8 | if !app_config_dir.exists() { 9 | if let Err(e) = fs::create_dir(&app_config_dir) { 10 | error!(error = ?e, "Failed to create config directory"); 11 | panic!("Failed to create config directory: {}", e); 12 | } else { 13 | info!(path = %app_config_dir.display(), "Config directory created successfully"); 14 | } 15 | } else { 16 | debug!(path = %app_config_dir.display(), "Config directory already exists"); 17 | } 18 | 19 | app_config_dir 20 | }); 21 | 22 | pub static DWALL_CACHE_DIR: LazyLock = LazyLock::new(|| { 23 | let config_dir = dirs::cache_dir().unwrap(); 24 | 25 | let dir = config_dir.join("com.thep0y.dwall"); // bundle identifier 26 | trace!(path = %dir.display(), "Initializing cache directory"); 27 | 28 | if !dir.exists() { 29 | if let Err(e) = fs::create_dir(&dir) { 30 | error!(error = ?e, "Failed to create cache directory"); 31 | panic!("Failed to create cache directory: {}", e); 32 | } else { 33 | info!("Cache directory created successfully at: {}", dir.display()); 34 | } 35 | } else { 36 | debug!(path = %dir.display(), "Cache directory already exists"); 37 | } 38 | 39 | dir 40 | }); 41 | -------------------------------------------------------------------------------- /daemon/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod color_mode; 2 | pub mod config; 3 | pub mod error; 4 | mod lazy; 5 | mod log; 6 | pub mod monitor; 7 | pub mod position; 8 | pub mod registry; 9 | mod solar; 10 | mod theme; 11 | pub mod utils; 12 | 13 | #[macro_use] 14 | extern crate tracing; 15 | 16 | pub use color_mode::ColorMode; 17 | pub use error::{DwallError, DwallResult}; 18 | pub use lazy::{DWALL_CACHE_DIR, DWALL_CONFIG_DIR}; 19 | pub use log::setup_logging; 20 | pub use theme::{apply_theme, ThemeValidator}; 21 | -------------------------------------------------------------------------------- /daemon/src/log.rs: -------------------------------------------------------------------------------- 1 | use std::{env, str::FromStr, sync::Mutex}; 2 | 3 | use time::{format_description::BorrowedFormatItem, macros::format_description}; 4 | use tracing::Level; 5 | use tracing_subscriber::{ 6 | fmt::{time::LocalTime, writer::BoxMakeWriter}, 7 | EnvFilter, 8 | }; 9 | 10 | /// Get default log level 11 | fn default_log_level() -> Level { 12 | if cfg!(debug_assertions) { 13 | Level::TRACE 14 | } else { 15 | Level::WARN 16 | } 17 | } 18 | 19 | /// Get log level from environment variable 20 | fn get_log_level() -> Level { 21 | env::var("DWALL_LOG") 22 | .ok() 23 | .as_deref() 24 | .or(option_env!("DWALL_LOG")) 25 | .and_then(|level| Level::from_str(level).ok()) 26 | .unwrap_or_else(default_log_level) 27 | } 28 | 29 | /// Create environment filter for logging 30 | fn create_env_filter>(pkg_names: &[S], level: Level) -> EnvFilter { 31 | let filter_str = pkg_names 32 | .iter() 33 | .map(|s| format!("{}={}", s.as_ref(), level)) 34 | .collect::>() 35 | .join(","); 36 | 37 | EnvFilter::builder() 38 | .with_default_directive(level.into()) 39 | .parse_lossy(filter_str) 40 | } 41 | 42 | /// Get time format based on build configuration 43 | fn get_time_format<'a>() -> &'a [BorrowedFormatItem<'a>] { 44 | if cfg!(debug_assertions) { 45 | format_description!("[hour]:[minute]:[second].[subsecond digits:3]") 46 | } else { 47 | format_description!("[year]-[month]-[day] [hour]:[minute]:[second].[subsecond digits:3]") 48 | } 49 | } 50 | 51 | /// Setup logging with given configuration 52 | pub fn setup_logging>(pkg_names: &[S]) { 53 | let timer = LocalTime::new(get_time_format()); 54 | let level = get_log_level(); 55 | 56 | let writer = if cfg!(debug_assertions) { 57 | BoxMakeWriter::new(Mutex::new(std::io::stderr())) 58 | } else { 59 | use crate::lazy::DWALL_CONFIG_DIR; 60 | use std::fs::File; 61 | 62 | let log_file = 63 | File::create(DWALL_CONFIG_DIR.join(format!("{}.log", pkg_names[0].as_ref()))) 64 | .expect("Failed to create the log file"); 65 | BoxMakeWriter::new(Mutex::new(log_file)) 66 | }; 67 | 68 | let builder = tracing_subscriber::fmt() 69 | .with_file(cfg!(debug_assertions)) 70 | .with_target(!cfg!(debug_assertions)) 71 | .with_line_number(true) 72 | .with_env_filter(create_env_filter(pkg_names, level)) 73 | .with_timer(timer) 74 | .with_writer(writer); 75 | 76 | if cfg!(debug_assertions) { 77 | builder.with_ansi(true).init(); 78 | } else { 79 | builder.json().init(); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /daemon/src/main.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] 2 | 3 | use dwall::{apply_theme, config::read_config_file, setup_logging}; 4 | 5 | #[tokio::main] 6 | async fn main() -> dwall::DwallResult<()> { 7 | setup_logging(&[env!("CARGO_PKG_NAME").replace("-", "_")]); 8 | 9 | let config = read_config_file().await?; 10 | apply_theme(config).await?; 11 | 12 | Ok(()) 13 | } 14 | -------------------------------------------------------------------------------- /daemon/src/monitor/error.rs: -------------------------------------------------------------------------------- 1 | use windows::{core::Error as WindowsError, Win32::Foundation::WIN32_ERROR}; 2 | 3 | /// Monitor operation related errors 4 | /// 5 | /// Contains all possible errors that may occur during interaction with monitor devices 6 | #[derive(Debug, thiserror::Error)] 7 | pub enum MonitorError { 8 | /// Unable to get monitor device collection 9 | #[error("Unable to get monitor device collection: {0:?}")] 10 | GetDeviceInfoSet(#[source] Option), 11 | 12 | /// Unable to get monitor device information 13 | #[error("Unable to get monitor device information: {0:?}")] 14 | GetDeviceInfo(#[source] WindowsError), 15 | 16 | /// Unable to get target device name 17 | #[error("Unable to get target device name: {0:?}")] 18 | GetTargetName(#[source] WindowsError), 19 | 20 | /// Unable to find matching device 21 | #[error("Unable to find matching device")] 22 | MatchDevice, 23 | 24 | /// Unable to get device friendly name 25 | #[error("Unable to get device friendly name: {0:?}")] 26 | GetFriendlyName(#[source] WindowsError), 27 | 28 | /// Unable to get buffer sizes 29 | #[error("Unable to get buffer sizes: {0:?}")] 30 | GetBufferSizes(WIN32_ERROR), 31 | 32 | /// Failed to query display configuration 33 | #[error("Failed to query display configuration: {0:?}")] 34 | QueryDisplayConfig(WIN32_ERROR), 35 | 36 | /// Failed to get device registry property 37 | #[error("Failed to get device registry property: {0:?}")] 38 | GetDeviceRegistryProperty(#[source] WindowsError), 39 | } 40 | -------------------------------------------------------------------------------- /daemon/src/position/geolocation_access.rs: -------------------------------------------------------------------------------- 1 | use windows::Devices::Geolocation::{GeolocationAccessStatus, Geolocator}; 2 | 3 | use crate::error::{DwallError, DwallResult}; 4 | 5 | use super::windows_api_helper::handle_windows_error; 6 | 7 | #[derive(Debug, thiserror::Error)] 8 | pub enum GeolocationAccessError { 9 | #[error("Geolocation permission was denied by the user")] 10 | Denied, 11 | #[error("Geolocation permission status is unspecified")] 12 | Unspecified, 13 | } 14 | 15 | /// Checks if the application has permission to access location 16 | /// 17 | /// Returns Ok(()) if permission is granted, or an error if denied or unspecified 18 | pub async fn check_location_permission() -> DwallResult<()> { 19 | let access_status = handle_windows_error("Requesting geolocation access permission", || { 20 | Geolocator::RequestAccessAsync() 21 | }) 22 | .await? 23 | .get() 24 | .map_err(|e| { 25 | error!(error = ?e, "Failed to get access status"); 26 | DwallError::Windows(e) 27 | })?; 28 | 29 | match access_status { 30 | GeolocationAccessStatus::Allowed => { 31 | debug!("Geolocation permission granted"); 32 | Ok(()) 33 | } 34 | GeolocationAccessStatus::Denied => { 35 | error!("{}", GeolocationAccessError::Denied); 36 | Err(GeolocationAccessError::Denied.into()) 37 | } 38 | GeolocationAccessStatus::Unspecified => { 39 | error!("{}", GeolocationAccessError::Unspecified); 40 | Err(GeolocationAccessError::Unspecified.into()) 41 | } 42 | _ => unreachable!(), 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /daemon/src/position/mod.rs: -------------------------------------------------------------------------------- 1 | //! Position module for handling geolocation functionality 2 | //! 3 | //! This module provides functionality for retrieving and managing geographical positions, 4 | //! including coordinate validation, geolocation access, and position caching. 5 | 6 | mod geolocation_access; 7 | mod position_manager; 8 | mod position_types; 9 | mod windows_api_helper; 10 | 11 | // Re-export public items 12 | pub use geolocation_access::{check_location_permission, GeolocationAccessError}; 13 | pub(crate) use position_manager::PositionManager; 14 | pub use position_types::{Position, PositionError}; 15 | -------------------------------------------------------------------------------- /daemon/src/position/position_types.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use crate::error::DwallResult; 4 | 5 | #[derive(Debug, Clone)] 6 | pub struct Position { 7 | latitude: f64, 8 | longitude: f64, 9 | } 10 | 11 | impl Position { 12 | /// Creates a new Position with the given latitude and longitude 13 | /// 14 | /// # Arguments 15 | /// * `latitude` - The latitude in degrees, must be between -90 and 90 16 | /// * `longitude` - The longitude in degrees, must be between -180 and 180 17 | /// 18 | /// # Returns 19 | /// A new Position instance if the coordinates are valid, or PositionError if they are out of range 20 | pub fn new(latitude: f64, longitude: f64) -> DwallResult { 21 | if !Self::is_valid_latitude(latitude) { 22 | return Err(PositionError::InvalidLatitude(latitude).into()); 23 | } 24 | if !Self::is_valid_longitude(longitude) { 25 | return Err(PositionError::InvalidLongitude(longitude).into()); 26 | } 27 | 28 | Ok(Position { 29 | latitude, 30 | longitude, 31 | }) 32 | } 33 | 34 | /// Creates a new Position without validation 35 | /// 36 | /// # Warning 37 | /// This method should only be used when the coordinates are already validated 38 | pub(crate) fn new_unchecked(latitude: f64, longitude: f64) -> Self { 39 | Position { 40 | latitude, 41 | longitude, 42 | } 43 | } 44 | 45 | /// Checks if the given latitude is valid (between -90 and 90 degrees) 46 | pub fn is_valid_latitude(latitude: f64) -> bool { 47 | (-90.0..=90.0).contains(&latitude) 48 | } 49 | 50 | /// Checks if the given longitude is valid (between -180 and 180 degrees) 51 | pub fn is_valid_longitude(longitude: f64) -> bool { 52 | (-180.0..=180.0).contains(&longitude) 53 | } 54 | 55 | pub fn latitude(&self) -> f64 { 56 | self.latitude 57 | } 58 | 59 | pub fn longitude(&self) -> f64 { 60 | self.longitude 61 | } 62 | } 63 | 64 | impl Default for Position { 65 | fn default() -> Self { 66 | // Default to coordinates (0, 0) - null island 67 | Self { 68 | latitude: 0.0, 69 | longitude: 0.0, 70 | } 71 | } 72 | } 73 | 74 | impl fmt::Display for Position { 75 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 76 | write!( 77 | f, 78 | "Position(lat: {}, lng: {})", 79 | self.latitude, self.longitude 80 | ) 81 | } 82 | } 83 | 84 | /// Error type for position-related operations 85 | #[derive(Debug, thiserror::Error)] 86 | pub enum PositionError { 87 | #[error("Invalid latitude: {0}. Must be between -90 and 90 degrees")] 88 | InvalidLatitude(f64), 89 | 90 | #[error("Invalid longitude: {0}. Must be between -180 and 180 degrees")] 91 | InvalidLongitude(f64), 92 | 93 | #[error("Invalid coordinates: latitude {0}, longitude {1}")] 94 | InvalidCoordinates(f64, f64), 95 | } 96 | 97 | #[cfg(test)] 98 | mod tests { 99 | use super::*; 100 | use tokio::test; 101 | 102 | #[test] 103 | async fn test_position_new() { 104 | let pos = Position::new(45.0, 90.0).unwrap(); 105 | assert_eq!(pos.latitude(), 45.0); 106 | assert_eq!(pos.longitude(), 90.0); 107 | } 108 | 109 | #[test] 110 | async fn test_position_new_invalid() { 111 | assert!(Position::new(91.0, 90.0).is_err()); 112 | assert!(Position::new(45.0, 181.0).is_err()); 113 | assert!(Position::new(-91.0, 0.0).is_err()); 114 | assert!(Position::new(0.0, -181.0).is_err()); 115 | } 116 | 117 | #[test] 118 | async fn test_position_validation() { 119 | assert!(Position::is_valid_latitude(90.0)); 120 | assert!(Position::is_valid_latitude(-90.0)); 121 | assert!(Position::is_valid_latitude(0.0)); 122 | assert!(!Position::is_valid_latitude(90.1)); 123 | assert!(!Position::is_valid_latitude(-90.1)); 124 | 125 | assert!(Position::is_valid_longitude(180.0)); 126 | assert!(Position::is_valid_longitude(-180.0)); 127 | assert!(Position::is_valid_longitude(0.0)); 128 | assert!(!Position::is_valid_longitude(180.1)); 129 | assert!(!Position::is_valid_longitude(-180.1)); 130 | } 131 | 132 | #[test] 133 | async fn test_position_default() { 134 | let pos = Position::default(); 135 | assert_eq!(pos.latitude(), 0.0); 136 | assert_eq!(pos.longitude(), 0.0); 137 | } 138 | 139 | #[test] 140 | async fn test_position_display() { 141 | let pos = Position::new_unchecked(45.0, 90.0); 142 | assert_eq!(format!("{}", pos), "Position(lat: 45, lng: 90)"); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /daemon/src/position/windows_api_helper.rs: -------------------------------------------------------------------------------- 1 | //! Windows API helper module 2 | 3 | use crate::error::{DwallError, DwallResult}; 4 | 5 | /// Helper function to handle Windows API errors with consistent logging 6 | /// 7 | /// This function maps a Windows error to a DwallError and logs the error message 8 | pub(super) async fn handle_windows_error(operation: &str, f: F) -> DwallResult 9 | where 10 | F: FnOnce() -> windows::core::Result, 11 | { 12 | trace!("{}", operation); 13 | match f() { 14 | Ok(result) => { 15 | debug!("{} completed successfully", operation); 16 | Ok(result) 17 | } 18 | Err(e) => { 19 | error!(error = ?e, "{} failed", operation); 20 | Err(DwallError::Windows(e)) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /daemon/src/theme/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::{config::Config, error::DwallResult}; 2 | 3 | use self::processor::ThemeProcessor; 4 | pub use self::validator::ThemeValidator; 5 | 6 | mod manager; 7 | mod processor; 8 | mod validator; 9 | 10 | /// Comprehensive error handling for theme-related operations 11 | #[derive(Debug, thiserror::Error)] 12 | pub enum ThemeError { 13 | #[error("Theme does not exist")] 14 | NotExists, 15 | #[error("Missing default theme")] 16 | MissingDefaultTheme, 17 | #[error("Missing solar configuration file")] 18 | MissingSolarConfigFile, 19 | #[error("Image count does not match solar configuration")] 20 | ImageCountMismatch, 21 | #[error("Wallpaper file does not exist")] 22 | MissingWallpaperFile, 23 | #[error("No monitor-specific wallpapers found")] 24 | NoMonitorSpecificWallpapers, 25 | } 26 | 27 | /// Applies a theme and starts a background task for periodic wallpaper updates 28 | pub async fn apply_theme(config: Config) -> DwallResult<()> { 29 | if config.monitor_specific_wallpapers().is_empty() { 30 | warn!("No monitor-specific wallpapers found, daemon will not be started"); 31 | return Err(ThemeError::NoMonitorSpecificWallpapers.into()); 32 | } 33 | 34 | let theme_processor = ThemeProcessor::new(&config)?; 35 | 36 | theme_processor.start_update_loop().await 37 | } 38 | -------------------------------------------------------------------------------- /daemon/src/theme/validator.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | use tokio::fs; 4 | 5 | use crate::{error::DwallResult, solar::SolarAngle, theme::ThemeError}; 6 | 7 | /// Theme validation utilities 8 | pub struct ThemeValidator; 9 | 10 | impl ThemeValidator { 11 | /// Checks if a theme exists and has valid configuration 12 | pub async fn validate_theme(themes_directory: &Path, theme_id: &str) -> DwallResult<()> { 13 | trace!(theme_id = theme_id, "Validating theme"); 14 | let theme_dir = themes_directory.join(theme_id); 15 | 16 | if !theme_dir.exists() { 17 | warn!(theme_id = theme_id, "Theme directory not found"); 18 | return Err(ThemeError::NotExists.into()); 19 | } 20 | 21 | let solar_angles = Self::read_solar_configuration(&theme_dir).await?; 22 | let image_indices: Vec = solar_angles.iter().map(|angle| angle.index).collect(); 23 | 24 | if !Self::validate_image_files(&theme_dir, &image_indices, "jpg") { 25 | warn!(theme_id = theme_id, "Image validation failed for theme"); 26 | return Err(ThemeError::ImageCountMismatch.into()); 27 | } 28 | 29 | debug!(theme_id = theme_id, "Theme validation successful"); 30 | Ok(()) 31 | } 32 | 33 | /// Reads solar configuration from theme directory 34 | async fn read_solar_configuration(theme_dir: &Path) -> DwallResult> { 35 | let solar_config_path = theme_dir.join("solar.json"); 36 | 37 | if !solar_config_path.exists() { 38 | error!(solar_config_path = %solar_config_path.display(), "Solar configuration file missing"); 39 | return Err(ThemeError::MissingSolarConfigFile.into()); 40 | } 41 | 42 | let solar_config_content = fs::read_to_string(&solar_config_path).await.map_err(|e| { 43 | error!(error = ?e, "Failed to read solar configuration"); 44 | e 45 | })?; 46 | 47 | let solar_angles: Vec = 48 | serde_json::from_str(&solar_config_content).map_err(|e| { 49 | error!(error = ?e, "Failed to parse solar configuration JSON"); 50 | e 51 | })?; 52 | 53 | debug!( 54 | solar_angles_count = solar_angles.len(), 55 | "Loaded solar angles from configuration" 56 | ); 57 | Ok(solar_angles) 58 | } 59 | 60 | /// Validates image files in the theme directory 61 | fn validate_image_files(theme_dir: &Path, indices: &[u8], image_format: &str) -> bool { 62 | let image_dir = theme_dir.join(image_format); 63 | 64 | if !image_dir.is_dir() { 65 | warn!(image_dir = %image_dir.display(), "Image directory not found"); 66 | return false; 67 | } 68 | 69 | let validation_result = indices.iter().all(|&index| { 70 | let image_filename = format!("{}.{}", index + 1, image_format); 71 | let image_path = image_dir.join(image_filename); 72 | 73 | let is_valid = image_path.exists() && image_path.is_file(); 74 | if !is_valid { 75 | warn!(image_path = %image_path.display(), "Missing or invalid image"); 76 | } 77 | is_valid 78 | }); 79 | 80 | validation_result 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /daemon/src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod string; 2 | -------------------------------------------------------------------------------- /images/home.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwall-rs/dwall/947d9e0e3a93b831b3c1b9c2354a652575c59b0e/images/home.avif -------------------------------------------------------------------------------- /images/settings.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwall-rs/dwall/947d9e0e3a93b831b3c1b9c2354a652575c59b0e/images/settings.avif -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | DWALL 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dwall-settings", 3 | "version": "0.1.24", 4 | "description": "", 5 | "type": "module", 6 | "scripts": { 7 | "build:vite": "vite build", 8 | "build:daemon": "cargo build -p dwall --release --features build-script,log-max-level-info", 9 | "build:daemon-dev": "cargo build -p dwall --features build-script,log-color", 10 | "build:daemon-debug": "cargo build -p dwall --release --features build-script", 11 | "build:daemon-stage": "cargo build -p dwall --release --features log-color,build-script", 12 | "build:debug": "tauri build --config src-tauri/tauri.debug.conf.json", 13 | "build": "tauri build --features log-max-level-info", 14 | "tauri": "tauri", 15 | "start": "vite", 16 | "start:dev": "cross-env RUST_BACKTRACE=1 tauri dev --features log-color", 17 | "dev": "bun run build:daemon-dev && bun run start:dev", 18 | "stage": "bun run build:daemon-stage && cross-env RUST_BACKTRACE=1 tauri dev --features log-color devtools --release", 19 | "serve": "vite preview", 20 | "check": "biome check --write src", 21 | "test": "vitest" 22 | }, 23 | "license": "MIT", 24 | "dependencies": { 25 | "@tauri-apps/api": "^2.5.0", 26 | "@tauri-apps/plugin-dialog": "^2.2.2", 27 | "@tauri-apps/plugin-process": "^2.2.1", 28 | "@tauri-apps/plugin-shell": "^2.2.1", 29 | "@tauri-apps/plugin-updater": "^2.7.1", 30 | "@vanilla-extract/css": "^1.17.2", 31 | "fluent-solid": "^0.2.1", 32 | "solid-icons": "^1.1.0", 33 | "solid-js": "^1.9.7" 34 | }, 35 | "devDependencies": { 36 | "@biomejs/biome": "^1.9.4", 37 | "@tauri-apps/cli": "^2.5.0", 38 | "@types/node": "^22.15.21", 39 | "@vanilla-extract/vite-plugin": "^5.0.2", 40 | "cross-env": "^7.0.3", 41 | "jsdom": "^26.1.0", 42 | "sass": "^1.89.0", 43 | "typescript": "^5.8.3", 44 | "vite": "^6.3.5", 45 | "vite-plugin-solid": "^2.11.6", 46 | "vitest": "^3.1.4" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src-tauri/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Generated by Tauri 6 | # will have schema files for capabilities auto-completion 7 | /gen/schemas 8 | -------------------------------------------------------------------------------- /src-tauri/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dwall-settings" 3 | description = "Dwall settings" 4 | edition.workspace = true 5 | version.workspace = true 6 | authors.workspace = true 7 | repository.workspace = true 8 | license.workspace = true 9 | 10 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 11 | 12 | [build-dependencies] 13 | tauri-build = { version = "2", features = [] } 14 | 15 | [dependencies] 16 | tauri = { version = "2", features = ["protocol-asset"] } 17 | dwall = { version = "0", path = "../daemon" } 18 | 19 | reqwest = { version = "0", default-features = false } 20 | futures-util = { version = "0", default-features = false } 21 | zip-extract = { version = "0", default-features = false, features = [ 22 | "deflate", 23 | ] } 24 | open = { version = "5", default-features = false } 25 | tokio = { workspace = true, features = ["macros", "process"] } 26 | dirs = { workspace = true } 27 | serde = { workspace = true } 28 | serde_json = { workspace = true } 29 | thiserror = { workspace = true } 30 | tracing = { workspace = true } 31 | tracing-subscriber = { workspace = true } 32 | windows = { workspace = true, features = [ 33 | "Win32_System_Registry", 34 | "Win32_System_Diagnostics_ToolHelp", 35 | "Win32_System_ProcessStatus", 36 | "Win32_Graphics_Dwm", 37 | "Wdk_System_SystemServices", 38 | "Win32_System_SystemInformation", 39 | "Win32_Globalization", 40 | "Win32_System_Threading", 41 | ] } 42 | rand = { version = "0.9", default-features = false, features = ["thread_rng"] } 43 | 44 | tauri-plugin-shell = "2" 45 | tauri-plugin-process = "2" 46 | 47 | [target.'cfg(target_os = "windows")'.dependencies] 48 | tauri-plugin-single-instance = "2" 49 | tauri-plugin-updater = "2" 50 | tauri-plugin-dialog = "2" 51 | 52 | [features] 53 | default = [] 54 | log-color = ["dwall/log-color"] 55 | devtools = ["tauri/devtools"] 56 | log-max-level-info = ["tracing/release_max_level_info"] 57 | -------------------------------------------------------------------------------- /src-tauri/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | tauri_build::build() 3 | } 4 | -------------------------------------------------------------------------------- /src-tauri/capabilities/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../gen/schemas/desktop-schema.json", 3 | "identifier": "default", 4 | "description": "Capability for the main window", 5 | "windows": ["main"], 6 | "permissions": [ 7 | "core:default", 8 | "updater:default", 9 | "dialog:default", 10 | "process:default", 11 | "shell:default" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /src-tauri/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwall-rs/dwall/947d9e0e3a93b831b3c1b9c2354a652575c59b0e/src-tauri/icons/128x128.png -------------------------------------------------------------------------------- /src-tauri/icons/128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwall-rs/dwall/947d9e0e3a93b831b3c1b9c2354a652575c59b0e/src-tauri/icons/128x128@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwall-rs/dwall/947d9e0e3a93b831b3c1b9c2354a652575c59b0e/src-tauri/icons/32x32.png -------------------------------------------------------------------------------- /src-tauri/icons/Square107x107Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwall-rs/dwall/947d9e0e3a93b831b3c1b9c2354a652575c59b0e/src-tauri/icons/Square107x107Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square142x142Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwall-rs/dwall/947d9e0e3a93b831b3c1b9c2354a652575c59b0e/src-tauri/icons/Square142x142Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square150x150Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwall-rs/dwall/947d9e0e3a93b831b3c1b9c2354a652575c59b0e/src-tauri/icons/Square150x150Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square284x284Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwall-rs/dwall/947d9e0e3a93b831b3c1b9c2354a652575c59b0e/src-tauri/icons/Square284x284Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square30x30Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwall-rs/dwall/947d9e0e3a93b831b3c1b9c2354a652575c59b0e/src-tauri/icons/Square30x30Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square310x310Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwall-rs/dwall/947d9e0e3a93b831b3c1b9c2354a652575c59b0e/src-tauri/icons/Square310x310Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square44x44Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwall-rs/dwall/947d9e0e3a93b831b3c1b9c2354a652575c59b0e/src-tauri/icons/Square44x44Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square71x71Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwall-rs/dwall/947d9e0e3a93b831b3c1b9c2354a652575c59b0e/src-tauri/icons/Square71x71Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square89x89Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwall-rs/dwall/947d9e0e3a93b831b3c1b9c2354a652575c59b0e/src-tauri/icons/Square89x89Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/StoreLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwall-rs/dwall/947d9e0e3a93b831b3c1b9c2354a652575c59b0e/src-tauri/icons/StoreLogo.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwall-rs/dwall/947d9e0e3a93b831b3c1b9c2354a652575c59b0e/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwall-rs/dwall/947d9e0e3a93b831b3c1b9c2354a652575c59b0e/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwall-rs/dwall/947d9e0e3a93b831b3c1b9c2354a652575c59b0e/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwall-rs/dwall/947d9e0e3a93b831b3c1b9c2354a652575c59b0e/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwall-rs/dwall/947d9e0e3a93b831b3c1b9c2354a652575c59b0e/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwall-rs/dwall/947d9e0e3a93b831b3c1b9c2354a652575c59b0e/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwall-rs/dwall/947d9e0e3a93b831b3c1b9c2354a652575c59b0e/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwall-rs/dwall/947d9e0e3a93b831b3c1b9c2354a652575c59b0e/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwall-rs/dwall/947d9e0e3a93b831b3c1b9c2354a652575c59b0e/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwall-rs/dwall/947d9e0e3a93b831b3c1b9c2354a652575c59b0e/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwall-rs/dwall/947d9e0e3a93b831b3c1b9c2354a652575c59b0e/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwall-rs/dwall/947d9e0e3a93b831b3c1b9c2354a652575c59b0e/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwall-rs/dwall/947d9e0e3a93b831b3c1b9c2354a652575c59b0e/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwall-rs/dwall/947d9e0e3a93b831b3c1b9c2354a652575c59b0e/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwall-rs/dwall/947d9e0e3a93b831b3c1b9c2354a652575c59b0e/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /src-tauri/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwall-rs/dwall/947d9e0e3a93b831b3c1b9c2354a652575c59b0e/src-tauri/icons/icon.icns -------------------------------------------------------------------------------- /src-tauri/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwall-rs/dwall/947d9e0e3a93b831b3c1b9c2354a652575c59b0e/src-tauri/icons/icon.ico -------------------------------------------------------------------------------- /src-tauri/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwall-rs/dwall/947d9e0e3a93b831b3c1b9c2354a652575c59b0e/src-tauri/icons/icon.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwall-rs/dwall/947d9e0e3a93b831b3c1b9c2354a652575c59b0e/src-tauri/icons/ios/AppIcon-20x20@1x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-20x20@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwall-rs/dwall/947d9e0e3a93b831b3c1b9c2354a652575c59b0e/src-tauri/icons/ios/AppIcon-20x20@2x-1.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwall-rs/dwall/947d9e0e3a93b831b3c1b9c2354a652575c59b0e/src-tauri/icons/ios/AppIcon-20x20@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwall-rs/dwall/947d9e0e3a93b831b3c1b9c2354a652575c59b0e/src-tauri/icons/ios/AppIcon-20x20@3x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwall-rs/dwall/947d9e0e3a93b831b3c1b9c2354a652575c59b0e/src-tauri/icons/ios/AppIcon-29x29@1x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-29x29@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwall-rs/dwall/947d9e0e3a93b831b3c1b9c2354a652575c59b0e/src-tauri/icons/ios/AppIcon-29x29@2x-1.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwall-rs/dwall/947d9e0e3a93b831b3c1b9c2354a652575c59b0e/src-tauri/icons/ios/AppIcon-29x29@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwall-rs/dwall/947d9e0e3a93b831b3c1b9c2354a652575c59b0e/src-tauri/icons/ios/AppIcon-29x29@3x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwall-rs/dwall/947d9e0e3a93b831b3c1b9c2354a652575c59b0e/src-tauri/icons/ios/AppIcon-40x40@1x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-40x40@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwall-rs/dwall/947d9e0e3a93b831b3c1b9c2354a652575c59b0e/src-tauri/icons/ios/AppIcon-40x40@2x-1.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwall-rs/dwall/947d9e0e3a93b831b3c1b9c2354a652575c59b0e/src-tauri/icons/ios/AppIcon-40x40@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwall-rs/dwall/947d9e0e3a93b831b3c1b9c2354a652575c59b0e/src-tauri/icons/ios/AppIcon-40x40@3x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwall-rs/dwall/947d9e0e3a93b831b3c1b9c2354a652575c59b0e/src-tauri/icons/ios/AppIcon-512@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwall-rs/dwall/947d9e0e3a93b831b3c1b9c2354a652575c59b0e/src-tauri/icons/ios/AppIcon-60x60@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwall-rs/dwall/947d9e0e3a93b831b3c1b9c2354a652575c59b0e/src-tauri/icons/ios/AppIcon-60x60@3x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwall-rs/dwall/947d9e0e3a93b831b3c1b9c2354a652575c59b0e/src-tauri/icons/ios/AppIcon-76x76@1x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwall-rs/dwall/947d9e0e3a93b831b3c1b9c2354a652575c59b0e/src-tauri/icons/ios/AppIcon-76x76@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dwall-rs/dwall/947d9e0e3a93b831b3c1b9c2354a652575c59b0e/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png -------------------------------------------------------------------------------- /src-tauri/src/cache/error.rs: -------------------------------------------------------------------------------- 1 | //! Error types for the cache module 2 | 3 | use std::path::PathBuf; 4 | use thiserror::Error; 5 | 6 | use crate::error::DwallSettingsError; 7 | 8 | /// Errors that can occur during cache operations 9 | #[derive(Debug, Error)] 10 | pub enum CacheError { 11 | /// Error occurred during HTTP request 12 | #[error(transparent)] 13 | Request(#[from] reqwest::Error), 14 | 15 | /// Error occurred during file system operations 16 | #[error(transparent)] 17 | Io(#[from] std::io::Error), 18 | 19 | /// Error occurred when downloading image 20 | #[error("Failed to download image after {retries} retries")] 21 | DownloadFailed { 22 | /// URL that failed to download 23 | url: String, 24 | /// Number of retries attempted 25 | retries: u32, 26 | }, 27 | 28 | /// Error occurred when cache file expected but not found 29 | #[error("Cached file not found: {0}")] 30 | CachedFileNotFound(PathBuf), 31 | 32 | /// Other unspecified errors 33 | #[error("{0}")] 34 | Other(String), 35 | } 36 | 37 | /// Result type for cache operations 38 | pub type CacheResult = std::result::Result; 39 | 40 | impl From for DwallSettingsError { 41 | fn from(value: CacheError) -> Self { 42 | match value { 43 | CacheError::Request(error) => DwallSettingsError::Request(error), 44 | CacheError::Io(error) => DwallSettingsError::Io(error), 45 | _ => DwallSettingsError::Other(value.to_string()), 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src-tauri/src/cache/fs_service.rs: -------------------------------------------------------------------------------- 1 | //! File system service for cache operations 2 | //! 3 | //! This module provides functionality for file system operations related to caching. 4 | 5 | use std::fs::File; 6 | use std::path::Path; 7 | 8 | use tokio::fs; 9 | 10 | use crate::fs::create_dir_if_missing; 11 | 12 | use super::error::CacheResult; 13 | 14 | /// Service for file system operations related to caching 15 | pub struct FsService; 16 | 17 | impl FsService { 18 | /// Ensure that the required directories exist 19 | pub async fn ensure_directories(theme_dir: &Path) -> CacheResult<()> { 20 | create_dir_if_missing(theme_dir).await.map_err(Into::into) 21 | } 22 | 23 | /// Update file access time to prevent premature expiration 24 | pub fn update_file_access_time(path: &Path) -> std::io::Result<()> { 25 | // Simply open and close the file to update access time 26 | File::open(path)?; 27 | Ok(()) 28 | } 29 | 30 | /// Check if a directory is empty 31 | pub async fn is_directory_empty(dir: &Path) -> bool { 32 | match fs::read_dir(dir).await { 33 | Ok(mut entries) => matches!(entries.next_entry().await, Ok(None)), 34 | Err(_) => false, 35 | } 36 | } 37 | 38 | /// Get the size of a directory recursively 39 | pub async fn get_directory_size(dir: &Path) -> CacheResult { 40 | let mut total_size = 0; 41 | let mut entries = fs::read_dir(dir).await.map_err(|e| { 42 | error!( 43 | dir = %dir.display(), 44 | error = ?e, 45 | "Failed to read directory" 46 | ); 47 | e 48 | })?; 49 | 50 | while let Ok(Some(entry)) = entries.next_entry().await { 51 | let path = entry.path(); 52 | 53 | if path.is_file() { 54 | if let Ok(metadata) = fs::metadata(&path).await { 55 | total_size += metadata.len(); 56 | } 57 | } else if path.is_dir() { 58 | total_size += Box::pin(Self::get_directory_size(&path)).await?; 59 | } 60 | } 61 | 62 | Ok(total_size) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src-tauri/src/cache/mod.rs: -------------------------------------------------------------------------------- 1 | //! Thumbnail cache functionality 2 | //! 3 | //! This module provides functionality for caching and managing thumbnail images. 4 | 5 | mod cache_manager; 6 | mod cleanup_service; 7 | mod error; 8 | mod fs_service; 9 | mod http_service; 10 | 11 | // Re-export the public API 12 | pub use self::cache_manager::{clear_thumbnail_cache, get_or_save_cached_thumbnails}; 13 | -------------------------------------------------------------------------------- /src-tauri/src/download/downloader.rs: -------------------------------------------------------------------------------- 1 | //! Theme download coordination 2 | //! 3 | //! This module coordinates the theme download process using various components. 4 | 5 | use std::path::PathBuf; 6 | 7 | use dwall::config::Config; 8 | use tauri::{Runtime, State, WebviewWindow}; 9 | 10 | use crate::error::DwallSettingsResult; 11 | 12 | use super::extractor::ThemeExtractor; 13 | use super::file_manager::ThemeFileManager; 14 | use super::http_service::HttpDownloadService; 15 | use super::task_manager::{DownloadTaskManager, ProgressEmitter}; 16 | 17 | /// Coordinates the theme download process 18 | pub struct ThemeDownloader { 19 | download_service: HttpDownloadService, 20 | task_manager: DownloadTaskManager, 21 | } 22 | 23 | impl ThemeDownloader { 24 | /// Create a new theme downloader 25 | pub fn new() -> Self { 26 | Self { 27 | download_service: HttpDownloadService::new(), 28 | task_manager: DownloadTaskManager::new(), 29 | } 30 | } 31 | 32 | /// Download theme zip file 33 | async fn download_theme( 34 | &self, 35 | config: &Config, 36 | theme_id: &str, 37 | progress_emitter: Option<&ProgressEmitter<'_, R>>, 38 | ) -> DwallSettingsResult { 39 | // Add download task and get the cancel flag 40 | let cancel_flag = self.task_manager.add_task(theme_id).await?; 41 | 42 | // Get file paths 43 | let (target_dir, temp_theme_zip_file, theme_zip_file) = 44 | ThemeFileManager::build_theme_paths(config, theme_id); 45 | 46 | // Prepare target directories 47 | ThemeFileManager::prepare_theme_directory(&target_dir).await?; 48 | 49 | // Construct download URL 50 | let github_url = HttpDownloadService::build_download_url(theme_id); 51 | let asset_url = config.github_asset_url(&github_url); 52 | 53 | // Download the file 54 | let download_result = self 55 | .download_service 56 | .download_file( 57 | &asset_url, 58 | &temp_theme_zip_file, 59 | theme_id, 60 | cancel_flag.clone(), 61 | progress_emitter, 62 | &self.task_manager, 63 | ) 64 | .await; 65 | 66 | // Handle download result 67 | match download_result { 68 | Ok(_) => { 69 | // Finalize the download 70 | ThemeFileManager::finalize_download(&temp_theme_zip_file, &theme_zip_file).await?; 71 | // Remove download task from tracking 72 | self.task_manager.remove_task(theme_id).await; 73 | Ok(theme_zip_file) 74 | } 75 | Err(e) => { 76 | // Only clean up temporary file if it's a non-resumable error or empty file 77 | if let Ok(metadata) = tokio::fs::metadata(&temp_theme_zip_file).await { 78 | if metadata.len() == 0 { 79 | // Clean up empty temporary file 80 | ThemeFileManager::cleanup_temp_file(&temp_theme_zip_file, false).await; 81 | } else { 82 | // Keep the partial download for future resume 83 | debug!( 84 | theme_id = theme_id, 85 | "Keeping partial download for future resume" 86 | ); 87 | } 88 | } 89 | // Remove download task from tracking 90 | self.task_manager.remove_task(theme_id).await; 91 | Err(e) 92 | } 93 | } 94 | } 95 | 96 | async fn cancel_theme_download(&self, theme_id: &str) { 97 | self.task_manager.cancel_task(theme_id).await; 98 | } 99 | } 100 | 101 | /// Download and extract a theme 102 | #[tauri::command] 103 | pub async fn download_theme_and_extract( 104 | window: WebviewWindow, 105 | downloader: State<'_, ThemeDownloader>, 106 | config: Config, 107 | theme_id: &str, 108 | ) -> DwallSettingsResult<()> { 109 | let progress_emitter = ProgressEmitter::new(&window); 110 | 111 | // Download theme 112 | let zip_path = downloader 113 | .download_theme(&config, theme_id, Some(&progress_emitter)) 114 | .await?; 115 | 116 | // Extract theme 117 | ThemeExtractor::extract_theme(config.themes_directory(), &zip_path, theme_id).await 118 | } 119 | 120 | /// Cancel an ongoing theme download 121 | #[tauri::command] 122 | pub async fn cancel_theme_download( 123 | downloader: State<'_, ThemeDownloader>, 124 | theme_id: String, 125 | ) -> DwallSettingsResult<()> { 126 | downloader.cancel_theme_download(&theme_id).await; 127 | Ok(()) 128 | } 129 | -------------------------------------------------------------------------------- /src-tauri/src/download/error.rs: -------------------------------------------------------------------------------- 1 | //! Error types for the download module 2 | 3 | use std::error::Error; 4 | 5 | #[derive(Debug, thiserror::Error)] 6 | pub enum DownloadError { 7 | #[error("{0}")] 8 | Connect(String), 9 | #[error("Download cancelled")] 10 | Cancelled, 11 | #[error("The theme does not exist on the server: {0}")] 12 | NotFound(String), 13 | #[error("Unhandled Error: {0}")] 14 | Unknown(String), 15 | } 16 | 17 | impl From for DownloadError { 18 | fn from(value: reqwest::Error) -> Self { 19 | let source = value 20 | .source() 21 | .map(|e| format!("{:?}", e)) 22 | .unwrap_or("".to_string()); 23 | 24 | if value.is_connect() { 25 | return Self::Connect(source[43..source.len() - 1].to_string()); 26 | } 27 | 28 | Self::Unknown(source) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src-tauri/src/download/extractor.rs: -------------------------------------------------------------------------------- 1 | //! Theme extraction functionality 2 | //! 3 | //! This module provides functionality for extracting downloaded theme archives. 4 | 5 | use std::path::Path; 6 | 7 | use tokio::fs; 8 | 9 | use crate::error::DwallSettingsResult; 10 | 11 | /// Theme extraction service 12 | pub struct ThemeExtractor; 13 | 14 | impl ThemeExtractor { 15 | /// Extract downloaded theme 16 | pub async fn extract_theme( 17 | themes_directory: &Path, 18 | zip_path: &Path, 19 | theme_id: &str, 20 | ) -> DwallSettingsResult<()> { 21 | let target_dir = themes_directory.join(theme_id); 22 | 23 | // Read downloaded file 24 | let archive = fs::read(zip_path).await.map_err(|e| { 25 | error!( 26 | theme_id = theme_id, 27 | zip_path = %zip_path.display(), 28 | error = ?e, 29 | "Failed to read theme archive" 30 | ); 31 | e 32 | })?; 33 | 34 | // Extract theme 35 | zip_extract::extract(std::io::Cursor::new(archive), &target_dir, true).map_err(|e| { 36 | error!( 37 | theme_id = theme_id, 38 | target_dir = %target_dir.display(), 39 | zip_path = %zip_path.display(), 40 | error = ?e, 41 | "Failed to extract theme archive" 42 | ); 43 | e 44 | })?; 45 | 46 | info!( 47 | theme_id = theme_id, 48 | target_dir = %target_dir.display(), 49 | "Successfully extracted theme" 50 | ); 51 | 52 | // Clean up zip file 53 | fs::remove_file(zip_path).await.map_err(|e| { 54 | error!( 55 | theme_id = theme_id, 56 | zip_path = %zip_path.display(), 57 | error = ?e, 58 | "Failed to delete theme archive" 59 | ); 60 | e 61 | })?; 62 | 63 | info!( 64 | theme_id = theme_id, 65 | zip_path = %zip_path.display(), 66 | "Deleted theme archive" 67 | ); 68 | Ok(()) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src-tauri/src/download/file_manager.rs: -------------------------------------------------------------------------------- 1 | //! Theme file management 2 | //! 3 | //! This module provides functionality for managing theme files and directories. 4 | 5 | use std::path::{Path, PathBuf}; 6 | 7 | use dwall::config::Config; 8 | use tokio::fs; 9 | 10 | use crate::error::DwallSettingsResult; 11 | 12 | /// Handles file system operations for theme management 13 | pub(super) struct ThemeFileManager; 14 | 15 | impl ThemeFileManager { 16 | /// Build paths for theme files 17 | pub(super) fn build_theme_paths( 18 | config: &Config, 19 | theme_id: &str, 20 | ) -> (PathBuf, PathBuf, PathBuf) { 21 | let target_dir = config.themes_directory().join(theme_id); 22 | let temp_theme_zip_file = config 23 | .themes_directory() 24 | .join(format!("{}.zip.temp", theme_id)); 25 | let theme_zip_file = config.themes_directory().join(format!("{}.zip", theme_id)); 26 | 27 | (target_dir, temp_theme_zip_file, theme_zip_file) 28 | } 29 | 30 | /// Prepare theme directory for download 31 | pub(super) async fn prepare_theme_directory(target_dir: &Path) -> DwallSettingsResult<()> { 32 | // Remove existing directory if it exists 33 | if target_dir.exists() { 34 | fs::remove_dir_all(target_dir).await.map_err(|e| { 35 | error!( 36 | dir_path = %target_dir.display(), 37 | error = ?e, 38 | "Failed to remove existing theme directory" 39 | ); 40 | e 41 | })?; 42 | trace!("Removed existing theme directory"); 43 | } 44 | 45 | // Create new directory 46 | fs::create_dir_all(target_dir).await.map_err(|e| { 47 | error!( 48 | dir_path = %target_dir.display(), 49 | error = ?e, 50 | "Failed to create theme directory" 51 | ); 52 | e 53 | })?; 54 | 55 | trace!(dir_path = %target_dir.display(), "Created new theme directory"); 56 | Ok(()) 57 | } 58 | 59 | /// Clean up temporary file if download is cancelled or failed 60 | /// 61 | /// If force_cleanup is false, the file will only be removed if it's empty 62 | pub async fn cleanup_temp_file(temp_file_path: &Path, force_cleanup: bool) { 63 | if temp_file_path.exists() { 64 | // Check if we should keep the file for resuming download 65 | if !force_cleanup { 66 | if let Ok(metadata) = fs::metadata(temp_file_path).await { 67 | if metadata.len() > 0 { 68 | debug!(file_path = %temp_file_path.display(), size = metadata.len(), "Keeping temporary file for resume"); 69 | return; 70 | } 71 | } 72 | } 73 | 74 | // Remove the file 75 | if let Err(e) = fs::remove_file(temp_file_path).await { 76 | error!( 77 | file_path = %temp_file_path.display(), 78 | error = ?e, 79 | "Failed to remove temporary file" 80 | ); 81 | } else { 82 | debug!(file_path = %temp_file_path.display(), "Removed temporary download file"); 83 | } 84 | } 85 | } 86 | 87 | /// Rename temporary file to final file 88 | pub(super) async fn finalize_download( 89 | temp_file_path: &Path, 90 | final_file_path: &Path, 91 | ) -> DwallSettingsResult<()> { 92 | fs::rename(temp_file_path, final_file_path) 93 | .await 94 | .map_err(|e| { 95 | error!( 96 | from = %temp_file_path.display(), 97 | to = %final_file_path.display(), 98 | error = ?e, 99 | "Failed to rename temporary file to final file" 100 | ); 101 | e.into() 102 | }) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src-tauri/src/download/mod.rs: -------------------------------------------------------------------------------- 1 | //! Theme download functionality 2 | //! 3 | //! This module provides functionality for downloading and extracting themes. 4 | 5 | mod downloader; 6 | mod error; 7 | mod extractor; 8 | mod file_manager; 9 | mod http_service; 10 | mod task_manager; 11 | 12 | pub use downloader::ThemeDownloader; 13 | pub use error::DownloadError; 14 | 15 | // Re-export the public API 16 | pub use self::downloader::{cancel_theme_download, download_theme_and_extract}; 17 | -------------------------------------------------------------------------------- /src-tauri/src/download/task_manager.rs: -------------------------------------------------------------------------------- 1 | //! Download task management 2 | //! 3 | //! This module provides functionality for managing download tasks and tracking progress. 4 | 5 | use std::collections::HashMap; 6 | use std::sync::atomic::{AtomicBool, Ordering}; 7 | use std::sync::{Arc, LazyLock}; 8 | 9 | use serde::Serialize; 10 | use tauri::{Emitter, Runtime, WebviewWindow}; 11 | use tokio::sync::Mutex; 12 | 13 | use crate::error::DwallSettingsResult; 14 | 15 | use super::error::DownloadError; 16 | 17 | /// Download task information 18 | #[derive(Debug)] 19 | pub(super) struct DownloadTask { 20 | /// Flag to indicate if the download should be cancelled 21 | pub cancel: Arc, 22 | } 23 | 24 | /// Download progress tracking 25 | #[derive(Serialize, Clone, Debug)] 26 | pub(super) struct DownloadProgress<'a> { 27 | pub theme_id: &'a str, 28 | pub downloaded_bytes: u64, 29 | pub total_bytes: u64, 30 | } 31 | 32 | /// Progress notification service 33 | pub(super) struct ProgressEmitter<'a, R: Runtime> { 34 | window: &'a WebviewWindow, 35 | } 36 | 37 | impl<'a, R: Runtime> ProgressEmitter<'a, R> { 38 | pub fn new(window: &'a WebviewWindow) -> Self { 39 | Self { window } 40 | } 41 | 42 | pub fn emit_progress(&self, progress: DownloadProgress) -> Result<(), tauri::Error> { 43 | self.window.emit("download-theme", progress) 44 | } 45 | } 46 | 47 | /// Manages download tasks and their cancellation flags 48 | pub(super) struct DownloadTaskManager { 49 | download_tasks: LazyLock>>>, 50 | } 51 | 52 | impl DownloadTaskManager { 53 | /// Create a new download task manager 54 | pub(super) fn new() -> Self { 55 | Self { 56 | download_tasks: LazyLock::new(|| Arc::new(Mutex::new(HashMap::new()))), 57 | } 58 | } 59 | 60 | /// Add a new download task and return its cancellation flag 61 | pub(super) async fn add_task(&self, theme_id: &str) -> DwallSettingsResult> { 62 | let mut tasks = self.download_tasks.lock().await; 63 | if tasks.contains_key(theme_id) { 64 | error!(theme_id = theme_id, "Theme is already being downloaded"); 65 | return Err( 66 | DownloadError::Unknown("Theme is already being downloaded".to_string()).into(), 67 | ); 68 | } 69 | 70 | // Mark theme as being downloaded with cancel flag 71 | let cancel_flag = Arc::new(AtomicBool::new(false)); 72 | tasks.insert( 73 | theme_id.to_string(), 74 | DownloadTask { 75 | cancel: cancel_flag.clone(), 76 | }, 77 | ); 78 | drop(tasks); 79 | 80 | Ok(cancel_flag) 81 | } 82 | 83 | /// Remove a download task 84 | pub(super) async fn remove_task(&self, theme_id: &str) { 85 | let mut tasks = self.download_tasks.lock().await; 86 | tasks.remove(theme_id); 87 | } 88 | 89 | /// Check if a task should be cancelled 90 | pub(super) fn is_cancelled(&self, cancel_flag: &Arc) -> bool { 91 | cancel_flag.load(Ordering::Relaxed) 92 | } 93 | 94 | /// Cancel a download task 95 | pub(super) async fn cancel_task(&self, theme_id: &str) { 96 | let tasks = self.download_tasks.lock().await; 97 | 98 | if let Some(task) = tasks.get(theme_id) { 99 | // Set the cancel flag to true 100 | task.cancel.store(true, Ordering::Relaxed); 101 | info!( 102 | theme_id = theme_id, 103 | "Requested cancellation of theme download" 104 | ); 105 | } else { 106 | // Theme is not being downloaded 107 | warn!( 108 | theme_id = theme_id, 109 | "Attempted to cancel download for theme that is not being downloaded" 110 | ); 111 | } 112 | 113 | drop(tasks); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src-tauri/src/error.rs: -------------------------------------------------------------------------------- 1 | use dwall::registry::RegistryError; 2 | use serde::{Serialize, Serializer}; 3 | 4 | use crate::download::DownloadError; 5 | 6 | pub type DwallSettingsResult = std::result::Result; 7 | 8 | #[derive(Debug, thiserror::Error)] 9 | pub enum DwallSettingsError { 10 | #[error(transparent)] 11 | Tauri(#[from] tauri::Error), 12 | #[error(transparent)] 13 | Update(#[from] tauri_plugin_updater::Error), 14 | #[error(transparent)] 15 | Request(#[from] reqwest::Error), 16 | #[error(transparent)] 17 | ZipExtract(#[from] zip_extract::ZipExtractError), 18 | #[error(transparent)] 19 | Dwall(#[from] dwall::DwallError), 20 | #[error(transparent)] 21 | Io(#[from] std::io::Error), 22 | #[error(transparent)] 23 | Windows(#[from] windows::core::Error), 24 | #[error(transparent)] 25 | Registry(#[from] RegistryError), 26 | #[error(transparent)] 27 | NulError(#[from] std::ffi::NulError), 28 | #[error(transparent)] 29 | Download(#[from] DownloadError), 30 | #[error("Failed to spawn daemon: {0}")] 31 | Daemon(String), 32 | #[error("{0}")] 33 | Other(String), 34 | } 35 | 36 | impl Serialize for DwallSettingsError { 37 | fn serialize(&self, serializer: S) -> std::result::Result 38 | where 39 | S: Serializer, 40 | { 41 | serializer.serialize_str(self.to_string().as_ref()) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src-tauri/src/i18n/locales/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod en_us; 2 | pub mod ja_jp; 3 | pub mod zh_cn; 4 | pub mod zh_hk; 5 | pub mod zh_tw; 6 | -------------------------------------------------------------------------------- /src-tauri/src/i18n/utils.rs: -------------------------------------------------------------------------------- 1 | use windows::{core::PWSTR, Win32::Globalization::GetUserPreferredUILanguages}; 2 | 3 | use crate::error::DwallSettingsResult; 4 | 5 | pub(super) fn get_user_preferred_language() -> DwallSettingsResult { 6 | trace!("Entering get_user_preferred_language function"); 7 | 8 | let mut num_languages: u32 = 0; 9 | let mut buffer_size: u32 = 0; 10 | 11 | unsafe { 12 | GetUserPreferredUILanguages( 13 | 0, // dwflags, 0 means no special options 14 | &mut num_languages, // Number of languages (output) 15 | None, // No buffer yet, to get the size first 16 | &mut buffer_size, 17 | )? 18 | }; 19 | 20 | debug!(buffer_size = buffer_size, "Initial buffer size obtained"); 21 | let mut buffer: Vec = vec![0; buffer_size as usize]; 22 | unsafe { 23 | GetUserPreferredUILanguages( 24 | 0, // dwflags, 0 means no special options 25 | &mut num_languages, // Number of languages (output) 26 | Some(PWSTR(buffer.as_mut_ptr())), // The buffer to hold the languages 27 | &mut buffer_size, // Buffer size (output) 28 | )?; 29 | } 30 | 31 | trace!( 32 | num_languages = num_languages, 33 | "Number of user preferred languages obtained" 34 | ); 35 | 36 | let languages = buffer 37 | .split(|&c| c == 0) 38 | .filter_map(|chunk| String::from_utf16(chunk).ok()) 39 | .collect::>(); 40 | 41 | if languages.is_empty() { 42 | warn!("No user preferred languages found, falling back to en-US"); 43 | return Ok("en-US".to_string()); 44 | } 45 | 46 | let language = &languages[0]; 47 | 48 | debug!( 49 | language = language, 50 | "Successfully got user preferred language" 51 | ); 52 | Ok(language.to_string()) 53 | } 54 | -------------------------------------------------------------------------------- /src-tauri/src/monitor.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use dwall::{ 4 | monitor::{Monitor, MonitorInfoProvider}, 5 | DwallResult, 6 | }; 7 | 8 | pub async fn get_monitors() -> DwallResult> { 9 | MonitorInfoProvider::new().get_monitors().await 10 | } 11 | -------------------------------------------------------------------------------- /src-tauri/src/postion.rs: -------------------------------------------------------------------------------- 1 | use crate::error::DwallSettingsResult; 2 | 3 | #[tauri::command] 4 | pub async fn request_location_permission() -> DwallSettingsResult<()> { 5 | dwall::position::check_location_permission() 6 | .await 7 | .map_err(Into::into) 8 | } 9 | -------------------------------------------------------------------------------- /src-tauri/src/setup.rs: -------------------------------------------------------------------------------- 1 | use std::{env, path::PathBuf, str::FromStr}; 2 | 3 | use tauri::Manager; 4 | 5 | use crate::{ 6 | download::ThemeDownloader, error::DwallSettingsError, process_manager::find_daemon_process, 7 | read_config_file, theme::spawn_apply_daemon, window::create_main_window, DAEMON_EXE_PATH, 8 | }; 9 | 10 | pub fn setup_app(app: &mut tauri::App) -> Result<(), Box> { 11 | info!( 12 | version = app.package_info().version.to_string(), 13 | build_mode = if cfg!(debug_assertions) { 14 | "debug" 15 | } else { 16 | "release" 17 | }, 18 | "Starting application" 19 | ); 20 | 21 | //#[cfg(all(desktop, not(debug_assertions)))] 22 | setup_updater(app)?; 23 | 24 | // Process launch arguments 25 | let args: Vec = env::args().collect(); 26 | debug!(arguments = ?args, "Launch arguments"); 27 | 28 | let settings_exe_path = PathBuf::from_str(&args[0])?; 29 | let daemon_exe_path = settings_exe_path 30 | .parent() 31 | .ok_or(DwallSettingsError::Io(std::io::ErrorKind::NotFound.into()))? 32 | .join("dwall.exe"); 33 | if !daemon_exe_path.exists() || !daemon_exe_path.is_file() { 34 | error!("Daemon executable does not exist"); 35 | return Err(Box::new(std::io::Error::from(std::io::ErrorKind::NotFound))); 36 | } 37 | info!(path = %daemon_exe_path.display(), "Found daemon exe"); 38 | DAEMON_EXE_PATH.set(daemon_exe_path)?; 39 | 40 | let theme_downloader = ThemeDownloader::new(); 41 | app.manage(theme_downloader); 42 | 43 | create_main_window(app.app_handle())?; 44 | 45 | // If a theme is configured in the configuration file but the background process is not detected, 46 | // then run the background process when this program starts. 47 | tauri::async_runtime::spawn(async move { 48 | let _ = read_config_file() 49 | .await 50 | .and_then(|_| find_daemon_process()) 51 | .and_then(|pid| pid.map_or_else(|| spawn_apply_daemon().map(|_| ()), |_| Ok(()))); 52 | }); 53 | 54 | info!("Application setup completed successfully"); 55 | 56 | Ok(()) 57 | } 58 | 59 | //#[cfg(all(desktop, not(debug_assertions)))] 60 | fn setup_updater(app: &mut tauri::App) -> Result<(), Box> { 61 | debug!("Initializing update plugin"); 62 | 63 | // Initialize update plugin 64 | app.handle() 65 | .plugin(tauri_plugin_updater::Builder::new().build()) 66 | .map_err(|e| { 67 | error!(error = ?e, "Failed to initialize update plugin"); 68 | e 69 | })?; 70 | 71 | Ok(()) 72 | } 73 | -------------------------------------------------------------------------------- /src-tauri/tauri.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "productName": "Dwall Settings", 3 | "version": "../package.json", 4 | "identifier": "com.thep0y.dwall", 5 | "build": { 6 | "beforeDevCommand": "bun run start", 7 | "devUrl": "http://localhost:1420", 8 | "beforeBuildCommand": "bun run build:vite && bun run build:daemon", 9 | "frontendDist": "../dist" 10 | }, 11 | "app": { 12 | "security": { 13 | "csp": "default-src 'self' ipc: http://ipc.localhost; img-src 'self' asset: http://asset.localhost", 14 | "assetProtocol": { 15 | "enable": true, 16 | "scope": ["$CACHE/com.thep0y.dwall/**"] 17 | } 18 | } 19 | }, 20 | "bundle": { 21 | "resources": { "../target/release/dwall.exe": "dwall.exe" }, 22 | "active": true, 23 | "copyright": "Copyright (C) 2025 thep0y. All rights reserved.", 24 | "createUpdaterArtifacts": true, 25 | "targets": "nsis", 26 | "icon": [ 27 | "icons/32x32.png", 28 | "icons/128x128.png", 29 | "icons/128x128@2x.png", 30 | "icons/icon.icns", 31 | "icons/icon.ico" 32 | ], 33 | "windows": { 34 | "nsis": { 35 | "languages": ["SimpChinese", "English"], 36 | "displayLanguageSelector": true, 37 | "installerIcon": "icons/icon.ico", 38 | "installerHooks": "./windows/hooks.nsi" 39 | } 40 | } 41 | }, 42 | "plugins": { 43 | "updater": { 44 | "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDcxQTQ5NDZBNUIyMEVDRTUKUldUbDdDQmJhcFNrY2RYSkpGNUt0U3cvdEozMXJoN2pXeEFBcUQ4YmZMTi9MS2E2YjNQT1pSbTgK", 45 | "endpoints": [ 46 | "https://app.thepoy.cc/dwall/latest-mirror-1.json", 47 | "https://app.thepoy.cc/dwall/latest-mirror-2.json", 48 | "https://gh-proxy.com/https://github.com/dwall-rs/dwall/releases/latest/download/latest.json", 49 | "https://github.com/dwall-rs/dwall/releases/latest/download/latest.json" 50 | ] 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src-tauri/tauri.debug.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "build": { 3 | "beforeBuildCommand": "bun run build:vite && bun run build:daemon-debug" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src-tauri/windows/hooks.nsi: -------------------------------------------------------------------------------- 1 | !include "StrFunc.nsh" 2 | 3 | !macro TerminateDwallProcess 4 | nsis_tauri_utils::FindProcess "dwall.exe" 5 | Pop $R0 6 | ${If} $R0 = 0 7 | DetailPrint "Found dwall.exe process running, attempting to terminate..." 8 | nsis_tauri_utils::KillProcess "dwall.exe" 9 | Pop $R0 10 | 11 | ${If} $R0 = 0 12 | DetailPrint "Successfully terminated dwall.exe process" 13 | ${Else} 14 | DetailPrint "Failed to terminate process using nsis_tauri_utils, trying taskkill..." 15 | # If above method failed, try using taskkill 16 | nsExec::ExecToStack `taskkill /F /IM dwall.exe` 17 | Pop $R1 18 | Pop $R2 19 | ${If} $R1 = 0 20 | DetailPrint "Successfully terminated dwall.exe process using taskkill" 21 | ${Else} 22 | DetailPrint "Failed to terminate dwall.exe process: $R2" 23 | ${EndIf} 24 | ${EndIf} 25 | 26 | # Add delay to ensure process is fully terminated 27 | DetailPrint "Waiting for process to fully terminate..." 28 | ${For} $R3 1 10 29 | nsis_tauri_utils::FindProcess "dwall.exe" 30 | Pop $R0 31 | ${If} $R0 != 0 32 | ${Break} 33 | ${EndIf} 34 | Sleep 200 35 | ${Next} 36 | ${EndIf} 37 | !macroend 38 | 39 | !macro NSIS_HOOK_PREINSTALL 40 | # NOTE: Clear incomplete thumbnails saved by old versions 41 | # ---- start: This hook will be removed in the future ---- 42 | RMDir /r "$LOCALAPPDATA\dwall" 43 | RMDir /r "$LOCALAPPDATA\com.thep0y.dwall" 44 | # ---- end ---- 45 | 46 | !insertmacro TerminateDwallProcess 47 | !macroend 48 | 49 | !macro NSIS_HOOK_PREUNINSTALL 50 | !insertmacro TerminateDwallProcess 51 | 52 | DeleteRegValue HKCU "Software\Microsoft\Windows\CurrentVersion\Run" "Dwall" 53 | !macroend 54 | 55 | !macro NSIS_HOOK_POSTUNINSTALL 56 | RMDir /r "$APPDATA\dwall" 57 | RMDir /r "$LOCALAPPDATA\dwall" 58 | !macroend -------------------------------------------------------------------------------- /src/App.css.ts: -------------------------------------------------------------------------------- 1 | import { style } from "@vanilla-extract/css"; 2 | import { themeContract } from "fluent-solid/lib/themes"; 3 | 4 | export const app = style({ 5 | height: "100vh", 6 | }); 7 | 8 | export const toastMessageLinkLikeButton = style({ 9 | backgroundColor: themeContract.colorTransparentBackground, 10 | border: "none", 11 | textDecoration: "underline", 12 | color: themeContract.colorBrandForegroundLink, 13 | cursor: "pointer", 14 | 15 | selectors: { 16 | "&:hover": { 17 | color: themeContract.colorBrandForegroundLinkHover, 18 | }, 19 | "&:active": { 20 | color: themeContract.colorBrandForegroundLinkPressed, 21 | }, 22 | }, 23 | }); 24 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { Show } from "solid-js"; 2 | 3 | import { LazyFlex } from "~/lazy"; 4 | 5 | import Settings from "./components/Settings"; 6 | import ThemeShowcase from "./components/ThemeShowcase"; 7 | import Updater from "./components/Update"; 8 | import Select from "./components/Select"; 9 | import Sidebar from "./components/Sidebar"; 10 | 11 | import useDark from "~/hooks/useDark"; 12 | import { useColorMode } from "./hooks/useColorMode"; 13 | import { useAppInitialization } from "./hooks/useAppInitialization"; 14 | 15 | import { useMonitor, useTheme, useTranslations, useSettings } from "~/contexts"; 16 | 17 | import * as styles from "./App.css"; 18 | 19 | const App = () => { 20 | const { translate } = useTranslations(); 21 | const theme = useTheme(); 22 | 23 | const { currentTheme, downloadThemeID, menuItemIndex, handleThemeSelection } = 24 | theme; 25 | 26 | const { 27 | list: monitors, 28 | handleChange: handleMonitorChange, 29 | id: monitorID, 30 | } = useMonitor(); 31 | const { showSettings } = useSettings(); 32 | 33 | useDark(); 34 | useColorMode(); 35 | 36 | useAppInitialization(translate, menuItemIndex, handleThemeSelection); 37 | 38 | return ( 39 | <> 40 | 41 | 42 | 43 | }> 44 | 45 |