初始提交
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