初始提交
This commit is contained in:
+13
@@ -0,0 +1,13 @@
|
||||
dist/
|
||||
build/
|
||||
*.spec
|
||||
logs/
|
||||
cache/
|
||||
assets/
|
||||
config.json
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
.vscode/
|
||||
.idea/
|
||||
*.egg-info/
|
||||
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env powershell
|
||||
# -*- coding: utf-8 -*-
|
||||
<#
|
||||
.SYNOPSIS
|
||||
壁纸更换器 - 编译脚本
|
||||
.DESCRIPTION
|
||||
使用 PyInstaller 将 Python 源码打包为单个 .exe 文件。
|
||||
输出: .\dist\WallpaperChanger.exe
|
||||
#>
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
|
||||
Write-Host "========================================" -ForegroundColor Cyan
|
||||
Write-Host " 壁纸更换器 - 编译打包" -ForegroundColor Cyan
|
||||
Write-Host "========================================" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
|
||||
Write-Host "[1/4] 激活 conda 环境 ..." -ForegroundColor Yellow
|
||||
conda activate 312
|
||||
if ($LASTEXITCODE -ne 0) { throw "激活 conda 环境失败 (312)" }
|
||||
|
||||
Write-Host "[2/4] 安装编译依赖 ..." -ForegroundColor Yellow
|
||||
python -m pip install pyinstaller --quiet
|
||||
if ($LASTEXITCODE -ne 0) { throw "安装 PyInstaller 失败" }
|
||||
|
||||
python -m pip install -r "$ScriptDir\requirements.txt" --quiet
|
||||
|
||||
Write-Host "[3/4] 清理旧产物 ..." -ForegroundColor Yellow
|
||||
Remove-Item -Recurse -Force "$ScriptDir\dist" -ErrorAction SilentlyContinue
|
||||
Remove-Item -Recurse -Force "$ScriptDir\build" -ErrorAction SilentlyContinue
|
||||
Remove-Item -Force "$ScriptDir\*.spec" -ErrorAction SilentlyContinue
|
||||
|
||||
Write-Host "[4/4] PyInstaller 打包 ..." -ForegroundColor Yellow
|
||||
Set-Location -LiteralPath $ScriptDir
|
||||
python -m PyInstaller `
|
||||
--onefile `
|
||||
--noconsole `
|
||||
--name "WallpaperChanger" `
|
||||
--clean `
|
||||
"$ScriptDir\main.py"
|
||||
|
||||
if ($LASTEXITCODE -ne 0) { throw "PyInstaller 打包失败" }
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "========================================" -ForegroundColor Green
|
||||
Write-Host " 编译完成!" -ForegroundColor Green
|
||||
Write-Host " 输出: $ScriptDir\dist\WallpaperChanger.exe" -ForegroundColor Green
|
||||
Write-Host "========================================" -ForegroundColor Green
|
||||
@@ -0,0 +1,103 @@
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def _get_app_dir():
|
||||
if getattr(sys, 'frozen', False):
|
||||
return os.path.dirname(sys.executable)
|
||||
return os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
||||
def _get_config_path():
|
||||
return os.path.join(_get_app_dir(), "config.json")
|
||||
|
||||
|
||||
DEFAULT_CONFIG = {
|
||||
"api": {
|
||||
"url": "http://localhost:8080/wallpaper",
|
||||
"mode": "direct",
|
||||
"json_image_field": "url",
|
||||
"timeout": 30,
|
||||
},
|
||||
"wallpaper": {
|
||||
"fill_style": "fill",
|
||||
"cache_dir": "./cache",
|
||||
"max_cache_files": 10,
|
||||
"show_notification": True,
|
||||
},
|
||||
"auto_refresh": {
|
||||
"enabled": False,
|
||||
"interval_minutes": 10,
|
||||
},
|
||||
}
|
||||
|
||||
_config = None
|
||||
|
||||
|
||||
def _resolve_path(path: str) -> str:
|
||||
if os.path.isabs(path):
|
||||
return path
|
||||
return os.path.normpath(os.path.join(_get_app_dir(), path))
|
||||
|
||||
|
||||
def load() -> dict:
|
||||
global _config
|
||||
if _config is not None:
|
||||
return _config
|
||||
config_path = _get_config_path()
|
||||
if os.path.exists(config_path):
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
_config = json.load(f)
|
||||
else:
|
||||
_config = DEFAULT_CONFIG
|
||||
merged = dict(DEFAULT_CONFIG)
|
||||
_deep_merge(merged, _config)
|
||||
_config = merged
|
||||
return _config
|
||||
|
||||
|
||||
def save(cfg: dict = None) -> None:
|
||||
global _config
|
||||
if cfg is not None:
|
||||
_config = cfg
|
||||
with open(_get_config_path(), "w", encoding="utf-8") as f:
|
||||
json.dump(_config, f, indent=4, ensure_ascii=False)
|
||||
|
||||
|
||||
def reload() -> dict:
|
||||
global _config
|
||||
_config = None
|
||||
return load()
|
||||
|
||||
|
||||
def get(path: str, default=None):
|
||||
cfg = load()
|
||||
keys = path.split(".")
|
||||
for k in keys:
|
||||
if isinstance(cfg, dict):
|
||||
cfg = cfg.get(k)
|
||||
if cfg is None:
|
||||
return default
|
||||
else:
|
||||
return default
|
||||
return cfg
|
||||
|
||||
|
||||
def set_(path: str, value):
|
||||
global _config
|
||||
cfg = load()
|
||||
keys = path.split(".")
|
||||
d = cfg
|
||||
for k in keys[:-1]:
|
||||
d = d.setdefault(k, {})
|
||||
d[keys[-1]] = value
|
||||
save(cfg)
|
||||
|
||||
|
||||
def _deep_merge(base, override):
|
||||
for key, value in override.items():
|
||||
if key in base and isinstance(base[key], dict) and isinstance(value, dict):
|
||||
_deep_merge(base[key], value)
|
||||
else:
|
||||
base[key] = value
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
import os
|
||||
import uuid
|
||||
import logging
|
||||
import mimetypes
|
||||
|
||||
import requests
|
||||
|
||||
import config
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
IMAGE_MIME_TYPES = {
|
||||
"image/jpeg",
|
||||
"image/png",
|
||||
"image/bmp",
|
||||
"image/gif",
|
||||
"image/webp",
|
||||
"image/tiff",
|
||||
}
|
||||
|
||||
EXT_MAP = {
|
||||
"image/jpeg": ".jpg",
|
||||
"image/png": ".png",
|
||||
"image/bmp": ".bmp",
|
||||
"image/gif": ".gif",
|
||||
"image/webp": ".webp",
|
||||
"image/tiff": ".tiff",
|
||||
}
|
||||
|
||||
|
||||
def _detect_ext(response) -> str:
|
||||
content_type = response.headers.get("Content-Type", "").split(";")[0].strip().lower()
|
||||
if content_type in EXT_MAP:
|
||||
return EXT_MAP[content_type]
|
||||
|
||||
url_path = response.url.split("?")[0]
|
||||
guessed, _ = mimetypes.guess_type(url_path)
|
||||
if guessed and guessed in EXT_MAP:
|
||||
return EXT_MAP[guessed]
|
||||
|
||||
return ".jpg"
|
||||
|
||||
|
||||
def fetch_wallpaper() -> str:
|
||||
cfg = config.load()
|
||||
api = cfg["api"]
|
||||
wallpaper_cfg = cfg["wallpaper"]
|
||||
|
||||
url = api["url"]
|
||||
mode = api.get("mode", "direct")
|
||||
timeout = api.get("timeout", 30)
|
||||
|
||||
cache_dir = wallpaper_cfg.get("cache_dir", "./cache")
|
||||
cache_dir = _resolve_cache(cache_dir)
|
||||
os.makedirs(cache_dir, exist_ok=True)
|
||||
|
||||
if mode == "json":
|
||||
image_url = _fetch_json(url, api.get("json_image_field", "url"), timeout)
|
||||
response = _download(image_url, timeout)
|
||||
else:
|
||||
response = _download(url, timeout)
|
||||
|
||||
ext = _detect_ext(response)
|
||||
filename = f"{uuid.uuid4().hex}{ext}"
|
||||
filepath = os.path.join(cache_dir, filename)
|
||||
|
||||
with open(filepath, "wb") as f:
|
||||
f.write(response.content)
|
||||
|
||||
logger.info("Wallpaper saved to: %s", filepath)
|
||||
max_cache = cfg["wallpaper"].get("max_cache_files", 10)
|
||||
_cleanup_cache(cache_dir, keep=max_cache)
|
||||
return filepath
|
||||
|
||||
|
||||
def _fetch_json(url: str, field: str, timeout: int) -> str:
|
||||
r = requests.get(url, timeout=timeout)
|
||||
r.raise_for_status()
|
||||
data = r.json()
|
||||
image_url = data.get(field)
|
||||
if not image_url:
|
||||
raise ValueError(f"Field '{field}' not found in JSON response")
|
||||
return image_url
|
||||
|
||||
|
||||
def _download(url: str, timeout: int) -> requests.Response:
|
||||
r = requests.get(url, timeout=timeout, stream=True)
|
||||
r.raise_for_status()
|
||||
return r
|
||||
|
||||
|
||||
def _resolve_cache(cache_dir: str) -> str:
|
||||
if os.path.isabs(cache_dir):
|
||||
return cache_dir
|
||||
base = os.path.dirname(os.path.abspath(__file__))
|
||||
return os.path.normpath(os.path.join(base, cache_dir))
|
||||
|
||||
|
||||
def _cleanup_cache(cache_dir: str, keep: int) -> None:
|
||||
try:
|
||||
files = [os.path.join(cache_dir, f) for f in os.listdir(cache_dir)]
|
||||
files = [f for f in files if os.path.isfile(f)]
|
||||
files.sort(key=os.path.getmtime, reverse=True)
|
||||
for old in files[keep:]:
|
||||
os.remove(old)
|
||||
except Exception:
|
||||
pass
|
||||
@@ -0,0 +1,88 @@
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
import winreg
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
REG_KEY = r"Software\Classes\Directory\Background\shell\RefreshWallpaper"
|
||||
COMMAND_KEY = REG_KEY + r"\command"
|
||||
|
||||
MENU_NAME = "刷新壁纸(&R)"
|
||||
|
||||
|
||||
def _get_app_dir() -> str:
|
||||
if getattr(sys, 'frozen', False):
|
||||
return os.path.dirname(sys.executable)
|
||||
return os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
||||
def _get_command() -> str:
|
||||
if getattr(sys, 'frozen', False):
|
||||
return f'"{sys.executable}" --refresh'
|
||||
else:
|
||||
base = os.path.dirname(sys.executable)
|
||||
pythonw = os.path.join(base, "pythonw.exe")
|
||||
if not os.path.exists(pythonw):
|
||||
pythonw = sys.executable
|
||||
script = os.path.join(_get_app_dir(), "refresh.py")
|
||||
return f'"{pythonw}" "{script}"'
|
||||
|
||||
|
||||
def _get_icon_path() -> str:
|
||||
return os.path.join(_get_app_dir(), "assets", "icon.ico")
|
||||
|
||||
|
||||
def install() -> bool:
|
||||
command = _get_command()
|
||||
icon = _get_icon_path()
|
||||
|
||||
try:
|
||||
key = winreg.CreateKey(winreg.HKEY_CURRENT_USER, REG_KEY)
|
||||
winreg.SetValueEx(key, "", 0, winreg.REG_SZ, MENU_NAME)
|
||||
if os.path.exists(icon):
|
||||
winreg.SetValueEx(key, "Icon", 0, winreg.REG_SZ, icon)
|
||||
winreg.CloseKey(key)
|
||||
|
||||
key = winreg.CreateKey(winreg.HKEY_CURRENT_USER, COMMAND_KEY)
|
||||
winreg.SetValueEx(key, "", 0, winreg.REG_SZ, command)
|
||||
winreg.CloseKey(key)
|
||||
|
||||
logger.info("Desktop right-click menu installed: %s", MENU_NAME)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error("Failed to install menu: %s", e)
|
||||
return False
|
||||
|
||||
|
||||
def uninstall() -> bool:
|
||||
try:
|
||||
key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, REG_KEY)
|
||||
winreg.CloseKey(key)
|
||||
except FileNotFoundError:
|
||||
logger.info("Menu not installed, nothing to uninstall")
|
||||
return True
|
||||
|
||||
try:
|
||||
winreg.DeleteKey(winreg.HKEY_CURRENT_USER, COMMAND_KEY)
|
||||
logger.info("Deleted registry key: %s", COMMAND_KEY)
|
||||
except Exception as e:
|
||||
logger.warning("Failed to delete command key: %s", e)
|
||||
|
||||
try:
|
||||
winreg.DeleteKey(winreg.HKEY_CURRENT_USER, REG_KEY)
|
||||
logger.info("Deleted registry key: %s", REG_KEY)
|
||||
except Exception as e:
|
||||
logger.error("Failed to delete menu key: %s", e)
|
||||
return False
|
||||
|
||||
logger.info("Desktop right-click menu uninstalled")
|
||||
return True
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
if len(sys.argv) > 1 and sys.argv[1] == "uninstall":
|
||||
uninstall()
|
||||
else:
|
||||
install()
|
||||
@@ -0,0 +1,113 @@
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import ctypes
|
||||
import logging
|
||||
|
||||
LOG_FORMAT = "%(asctime)s [%(levelname)s] %(name)s: %(message)s"
|
||||
LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
|
||||
|
||||
MUTEX_NAME = r"Global\WallpaperChanger_SingleInstance"
|
||||
ERROR_ALREADY_EXISTS = 183
|
||||
|
||||
|
||||
def _bootstrap():
|
||||
import config
|
||||
|
||||
app_dir = config._get_app_dir()
|
||||
for d in ["assets", "cache", "logs"]:
|
||||
os.makedirs(os.path.join(app_dir, d), exist_ok=True)
|
||||
|
||||
config_path = config._get_config_path()
|
||||
if not os.path.exists(config_path):
|
||||
with open(config_path, "w", encoding="utf-8") as f:
|
||||
json.dump(config.DEFAULT_CONFIG, f, indent=4, ensure_ascii=False)
|
||||
|
||||
|
||||
def _do_refresh():
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format=LOG_FORMAT,
|
||||
datefmt=LOG_DATE_FORMAT,
|
||||
)
|
||||
logger = logging.getLogger("refresh")
|
||||
|
||||
import config
|
||||
import fetcher
|
||||
import wallpaper
|
||||
|
||||
try:
|
||||
image_path = fetcher.fetch_wallpaper()
|
||||
fill_style = config.get("wallpaper.fill_style", "fill")
|
||||
wallpaper.set_wallpaper(image_path, fill_style)
|
||||
logger.info("Wallpaper updated successfully")
|
||||
except Exception as e:
|
||||
logger.error("Refresh failed: %s", e)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def _setup_logging():
|
||||
import config
|
||||
|
||||
log_dir = os.path.join(config._get_app_dir(), "logs")
|
||||
os.makedirs(log_dir, exist_ok=True)
|
||||
log_file = os.path.join(log_dir, "app.log")
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format=LOG_FORMAT,
|
||||
datefmt=LOG_DATE_FORMAT,
|
||||
handlers=[
|
||||
logging.FileHandler(log_file, encoding="utf-8"),
|
||||
logging.StreamHandler(sys.stdout),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def _ensure_icon():
|
||||
import config
|
||||
|
||||
icon_path = os.path.join(config._get_app_dir(), "assets", "icon.ico")
|
||||
if os.path.exists(icon_path):
|
||||
return
|
||||
from PIL import Image
|
||||
|
||||
img = Image.new("RGB", (64, 64), color=(66, 133, 244))
|
||||
sizes = [(16, 16), (32, 32), (48, 48), (64, 64)]
|
||||
img.save(icon_path, format="ICO", sizes=sizes)
|
||||
|
||||
|
||||
def _check_single_instance():
|
||||
handle = ctypes.windll.kernel32.CreateMutexW(None, False, MUTEX_NAME)
|
||||
if ctypes.windll.kernel32.GetLastError() == ERROR_ALREADY_EXISTS:
|
||||
ctypes.windll.user32.MessageBoxW(0, "程序已运行", "壁纸更换器", 0x40)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def main():
|
||||
_bootstrap()
|
||||
|
||||
if "--refresh" in sys.argv:
|
||||
_do_refresh()
|
||||
return
|
||||
|
||||
if not _check_single_instance():
|
||||
return
|
||||
|
||||
_setup_logging()
|
||||
logger = logging.getLogger("main")
|
||||
|
||||
try:
|
||||
_ensure_icon()
|
||||
except Exception as e:
|
||||
logger.warning("Failed to generate icon: %s", e)
|
||||
|
||||
from tray_app import TrayApp
|
||||
|
||||
app = TrayApp()
|
||||
logger.info("Starting Wallpaper Changer...")
|
||||
app.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,61 @@
|
||||
# 壁纸更换器
|
||||
|
||||
从 HTTP API 自动拉取壁纸并设置为 Windows 桌面背景,支持托盘常驻和桌面右键菜单。
|
||||
|
||||
## 功能
|
||||
|
||||
- 托盘程序常驻,右键菜单手动/自动刷新壁纸
|
||||
- 桌面右键菜单「刷新壁纸」一键更换
|
||||
- 支持 API 直接返回图片二进制或 JSON 含图片 URL 两种模式
|
||||
- 可配置填充方式(填充/适应/拉伸/平铺/居中)
|
||||
- 可配置缓存目录及最大缓存数量
|
||||
- 可开关换壁纸后气泡通知
|
||||
- 单实例运行,重复启动弹窗提示
|
||||
- 首次运行自动创建配置和目录
|
||||
|
||||
## 使用
|
||||
|
||||
```powershell
|
||||
# 运行托盘程序
|
||||
python main.py
|
||||
|
||||
# 右键菜单刷新
|
||||
python main.py --refresh
|
||||
|
||||
# 安装桌面右键菜单(首次需要,从托盘菜单操作)
|
||||
右键托盘 → 安装右键菜单
|
||||
```
|
||||
|
||||
## 编译为 .exe
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File build.ps1
|
||||
```
|
||||
|
||||
产物:`dist/WallpaperChanger.exe`
|
||||
|
||||
- 双击运行 → 启动托盘
|
||||
- `WallpaperChanger.exe --refresh` → 拉取并设置壁纸后退出(右键菜单用)
|
||||
- 首次运行在同目录下自动创建 `config.json`、`cache/`、`logs/`、`assets/`
|
||||
|
||||
## 配置
|
||||
|
||||
`config.json` 字段说明:
|
||||
|
||||
| 路径 | 类型 | 默认值 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `api.url` | string | — | 壁纸 API 地址 |
|
||||
| `api.mode` | string | `direct` | `direct` 返回图片二进制 / `json` 返回 JSON |
|
||||
| `api.json_image_field` | string | `url` | JSON 模式下图片 URL 的字段名 |
|
||||
| `api.timeout` | int | `30` | 请求超时秒数 |
|
||||
| `wallpaper.fill_style` | string | `fill` | `fill`/`fit`/`stretch`/`tile`/`center` |
|
||||
| `wallpaper.cache_dir` | string | `./cache` | 壁纸缓存目录 |
|
||||
| `wallpaper.max_cache_files` | int | `10` | 最大缓存文件数 |
|
||||
| `wallpaper.show_notification` | bool | `true` | 换壁纸后是否弹气泡通知 |
|
||||
| `auto_refresh.enabled` | bool | `false` | 是否开启定时自动刷新 |
|
||||
| `auto_refresh.interval_minutes` | int | `10` | 自动刷新间隔分钟数 |
|
||||
|
||||
## 依赖
|
||||
|
||||
- Python 3.12+
|
||||
- requests, pystray, Pillow
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
import sys
|
||||
import os
|
||||
import logging
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
import config
|
||||
import fetcher
|
||||
import wallpaper
|
||||
|
||||
|
||||
def main():
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s [%(levelname)s] %(message)s",
|
||||
datefmt="%Y-%m-%d %H:%M:%S",
|
||||
)
|
||||
logger = logging.getLogger("refresh")
|
||||
|
||||
try:
|
||||
logger.info("Fetching wallpaper...")
|
||||
image_path = fetcher.fetch_wallpaper()
|
||||
fill_style = config.get("wallpaper.fill_style", "fill")
|
||||
wallpaper.set_wallpaper(image_path, fill_style)
|
||||
logger.info("Wallpaper updated successfully")
|
||||
except Exception as e:
|
||||
logger.error("Refresh failed: %s", e)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,3 @@
|
||||
requests>=2.28.0
|
||||
pystray>=0.19.0
|
||||
Pillow>=9.0.0
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
import os
|
||||
import threading
|
||||
import logging
|
||||
|
||||
from PIL import Image
|
||||
import pystray
|
||||
|
||||
import config
|
||||
import fetcher
|
||||
import wallpaper
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
STYLES = ["fill", "fit", "stretch", "tile", "center"]
|
||||
STYLE_LABELS = {"fill": "填充", "fit": "适应", "stretch": "拉伸", "tile": "平铺", "center": "居中"}
|
||||
|
||||
|
||||
def _get_icon_path():
|
||||
return os.path.join(config._get_app_dir(), "assets", "icon.ico")
|
||||
|
||||
|
||||
class TrayApp:
|
||||
def __init__(self):
|
||||
self._icon = None
|
||||
self._auto_timer = None
|
||||
self._running = False
|
||||
|
||||
def run(self):
|
||||
self._running = True
|
||||
image = Image.open(_get_icon_path())
|
||||
self._icon = pystray.Icon(
|
||||
"WallpaperChanger",
|
||||
image,
|
||||
"壁纸更换器",
|
||||
menu=self._build_menu(),
|
||||
)
|
||||
self._icon.title = "壁纸更换器"
|
||||
if config.get("auto_refresh.enabled"):
|
||||
self._start_auto_refresh()
|
||||
self._icon.run()
|
||||
|
||||
def stop(self):
|
||||
self._running = False
|
||||
self._stop_auto_refresh()
|
||||
if self._icon:
|
||||
self._icon.stop()
|
||||
|
||||
def _build_menu(self):
|
||||
show_notify = config.get("wallpaper.show_notification", True)
|
||||
notify_label = "弹出通知: 开" if show_notify else "弹出通知: 关"
|
||||
return pystray.Menu(
|
||||
pystray.MenuItem("刷新壁纸", self._on_refresh, default=True),
|
||||
pystray.MenuItem("自动刷新", self._auto_refresh_submenu()),
|
||||
pystray.MenuItem("填充方式", self._fill_style_submenu()),
|
||||
pystray.Menu.SEPARATOR,
|
||||
pystray.MenuItem(notify_label, self._on_toggle_notification),
|
||||
pystray.Menu.SEPARATOR,
|
||||
pystray.MenuItem("安装右键菜单", self._on_install_menu),
|
||||
pystray.MenuItem("卸载右键菜单", self._on_uninstall_menu),
|
||||
pystray.Menu.SEPARATOR,
|
||||
pystray.MenuItem("退出", self._on_exit),
|
||||
)
|
||||
|
||||
def _auto_refresh_submenu(self):
|
||||
enabled = config.get("auto_refresh.enabled", False)
|
||||
label = "状态: 开启" if enabled else "状态: 关闭"
|
||||
return pystray.Menu(
|
||||
pystray.MenuItem(label, None),
|
||||
pystray.Menu.SEPARATOR,
|
||||
pystray.MenuItem("开启", self._on_auto_on),
|
||||
pystray.MenuItem("关闭", self._on_auto_off),
|
||||
)
|
||||
|
||||
def _fill_style_submenu(self):
|
||||
items = []
|
||||
for s in STYLES:
|
||||
items.append(
|
||||
pystray.MenuItem(
|
||||
STYLE_LABELS[s],
|
||||
self._on_set_fill_style,
|
||||
checked=lambda item, _s=s: _s == config.get("wallpaper.fill_style", "fill"),
|
||||
)
|
||||
)
|
||||
return pystray.Menu(*items)
|
||||
|
||||
def _on_refresh(self, icon, item=None):
|
||||
threading.Thread(target=self._do_refresh, daemon=True).start()
|
||||
|
||||
def _do_refresh(self):
|
||||
try:
|
||||
image_path = fetcher.fetch_wallpaper()
|
||||
fill_style = config.get("wallpaper.fill_style", "fill")
|
||||
ok = wallpaper.set_wallpaper(image_path, fill_style)
|
||||
if ok and self._icon:
|
||||
if config.get("wallpaper.show_notification", True):
|
||||
self._icon.notify("壁纸已更新")
|
||||
except Exception as e:
|
||||
logger.exception("Refresh failed")
|
||||
if self._icon and config.get("wallpaper.show_notification", True):
|
||||
self._icon.notify(f"刷新失败: {e}")
|
||||
|
||||
def _on_auto_on(self, icon, item):
|
||||
config.set_("auto_refresh.enabled", True)
|
||||
self._start_auto_refresh()
|
||||
icon.update_menu()
|
||||
|
||||
def _on_auto_off(self, icon, item):
|
||||
config.set_("auto_refresh.enabled", False)
|
||||
self._stop_auto_refresh()
|
||||
icon.update_menu()
|
||||
|
||||
def _on_set_fill_style(self, icon, item):
|
||||
for s in STYLES:
|
||||
if STYLE_LABELS[s] == str(item):
|
||||
config.set_("wallpaper.fill_style", s)
|
||||
break
|
||||
icon.update_menu()
|
||||
|
||||
def _on_toggle_notification(self, icon, item):
|
||||
current = config.get("wallpaper.show_notification", True)
|
||||
config.set_("wallpaper.show_notification", not current)
|
||||
icon.update_menu()
|
||||
|
||||
def _on_install_menu(self, icon, item):
|
||||
threading.Thread(target=self._do_install_menu, daemon=True).start()
|
||||
|
||||
def _do_install_menu(self):
|
||||
try:
|
||||
from install_menu import install
|
||||
install()
|
||||
if self._icon:
|
||||
self._icon.notify("右键菜单已安装")
|
||||
except Exception as e:
|
||||
logger.exception("Install menu failed")
|
||||
if self._icon:
|
||||
self._icon.notify(f"安装失败: {e}")
|
||||
|
||||
def _on_uninstall_menu(self, icon, item):
|
||||
threading.Thread(target=self._do_uninstall_menu, daemon=True).start()
|
||||
|
||||
def _do_uninstall_menu(self):
|
||||
try:
|
||||
from install_menu import uninstall
|
||||
uninstall()
|
||||
if self._icon:
|
||||
self._icon.notify("右键菜单已卸载")
|
||||
except Exception as e:
|
||||
logger.exception("Uninstall menu failed")
|
||||
if self._icon:
|
||||
self._icon.notify(f"卸载失败: {e}")
|
||||
|
||||
def _on_exit(self, icon, item):
|
||||
self.stop()
|
||||
|
||||
def _start_auto_refresh(self):
|
||||
self._stop_auto_refresh()
|
||||
interval = config.get("auto_refresh.interval_minutes", 10) * 60
|
||||
self._auto_timer = threading.Timer(interval, self._auto_refresh_tick)
|
||||
self._auto_timer.daemon = True
|
||||
self._auto_timer.start()
|
||||
logger.info("Auto refresh started, interval=%ds", interval)
|
||||
|
||||
def _stop_auto_refresh(self):
|
||||
if self._auto_timer:
|
||||
self._auto_timer.cancel()
|
||||
self._auto_timer = None
|
||||
|
||||
def _auto_refresh_tick(self):
|
||||
if not self._running:
|
||||
return
|
||||
self._do_refresh()
|
||||
if config.get("auto_refresh.enabled"):
|
||||
interval = config.get("auto_refresh.interval_minutes", 10) * 60
|
||||
self._auto_timer = threading.Timer(interval, self._auto_refresh_tick)
|
||||
self._auto_timer.daemon = True
|
||||
self._auto_timer.start()
|
||||
@@ -0,0 +1,63 @@
|
||||
import ctypes
|
||||
import os
|
||||
import logging
|
||||
from ctypes import wintypes
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
SPI_SETDESKWALLPAPER = 0x0014
|
||||
SPIF_UPDATEINIFILE = 0x01
|
||||
SPIF_SENDWININICHANGE = 0x02
|
||||
|
||||
FILL_STYLES = {
|
||||
"fill": {"WallpaperStyle": "10", "TileWallpaper": "0"},
|
||||
"fit": {"WallpaperStyle": "6", "TileWallpaper": "0"},
|
||||
"stretch": {"WallpaperStyle": "2", "TileWallpaper": "0"},
|
||||
"tile": {"WallpaperStyle": "0", "TileWallpaper": "1"},
|
||||
"center": {"WallpaperStyle": "0", "TileWallpaper": "0"},
|
||||
}
|
||||
|
||||
|
||||
def set_fill_style(style: str) -> None:
|
||||
import winreg
|
||||
|
||||
values = FILL_STYLES.get(style)
|
||||
if not values:
|
||||
values = FILL_STYLES["fill"]
|
||||
|
||||
try:
|
||||
key = winreg.OpenKey(
|
||||
winreg.HKEY_CURRENT_USER,
|
||||
r"Control Panel\Desktop",
|
||||
0,
|
||||
winreg.KEY_SET_VALUE,
|
||||
)
|
||||
winreg.SetValueEx(key, "WallpaperStyle", 0, winreg.REG_SZ, values["WallpaperStyle"])
|
||||
winreg.SetValueEx(key, "TileWallpaper", 0, winreg.REG_SZ, values["TileWallpaper"])
|
||||
winreg.CloseKey(key)
|
||||
logger.info("Fill style set to: %s", style)
|
||||
except Exception as e:
|
||||
logger.error("Failed to set fill style: %s", e)
|
||||
|
||||
|
||||
def set_wallpaper(image_path: str, fill_style: str = "fill") -> bool:
|
||||
if not os.path.exists(image_path):
|
||||
logger.error("Image not found: %s", image_path)
|
||||
return False
|
||||
|
||||
set_fill_style(fill_style)
|
||||
|
||||
abs_path = os.path.abspath(image_path)
|
||||
result = ctypes.windll.user32.SystemParametersInfoW(
|
||||
SPI_SETDESKWALLPAPER,
|
||||
0,
|
||||
abs_path,
|
||||
SPIF_UPDATEINIFILE | SPIF_SENDWININICHANGE,
|
||||
)
|
||||
|
||||
if result:
|
||||
logger.info("Wallpaper set: %s", abs_path)
|
||||
else:
|
||||
logger.error("Failed to set wallpaper: %s", abs_path)
|
||||
|
||||
return bool(result)
|
||||
Reference in New Issue
Block a user