feat: decompose plugin and settings into service-oriented architecture

Extract 6 core services from the monolithic plugin.ts (819→284 lines):
- EventBus: unified typed event system replacing 3 ad-hoc notification mechanisms
- TimeRuleScheduler: pure time-rule matching and scheduling logic
- StyleManager: CSS variable injection, image analysis with size caching
- BackgroundManager: lifecycle, timer management, and background selection
- BackgroundPersistence: immutable vault file I/O (no longer mutates BackgroundItem)
- Logger: centralized logging with consistent prefix

Decompose settings-tab.ts (1417→147 lines) into 4 focused sections:
- BasicSettingsSection, ModeSettingsSection, BgManagementSection, ApiSettingsSection

Additional improvements:
- Enable TypeScript strict mode
- Pin all devDependency versions for reproducible builds
- Template method pattern for provider updateImageCache/deinit/fetchPage
- Move safeString() to BaseWallpaperApi, consolidate ID generation
- Fix 26 bugs: deleteApi missing deinit, getImages infinite loop, settings-view
  memory leak, manual mode bounds check, statusBar DOM leak, test API ID collision,
  rule lookup by .name→.id, DragSort cleanup, perPage ordering, and more
This commit is contained in:
sean2077 2026-03-17 18:32:02 +08:00
parent 198e3d2e56
commit 62d855ffbd
No known key found for this signature in database
GPG key ID: 829573CF26B96C25
33 changed files with 2702 additions and 2363 deletions

View file

@ -17,23 +17,23 @@
"author": "sean2077",
"license": "MIT",
"devDependencies": {
"@types/node": "latest",
"@typescript-eslint/eslint-plugin": "latest",
"@typescript-eslint/parser": "latest",
"@types/node": "^24.10.1",
"@typescript-eslint/eslint-plugin": "^8.48.0",
"@typescript-eslint/parser": "^8.48.0",
"all-contributors-cli": "^6.26.1",
"builtin-modules": "latest",
"esbuild": "latest",
"eslint": "latest",
"eslint-plugin-obsidianmd": "latest",
"obsidian": "latest",
"prettier": "latest",
"stylelint": "latest",
"stylelint-config-idiomatic-order": "latest",
"stylelint-config-recommended": "latest",
"stylelint-config-standard": "latest",
"stylelint-order": "latest",
"tslib": "latest",
"typescript": "latest"
"builtin-modules": "^5.0.0",
"esbuild": "^0.27.0",
"eslint": "^9.39.1",
"eslint-plugin-obsidianmd": "^0.1.9",
"obsidian": "^1.10.3",
"prettier": "^3.7.3",
"stylelint": "^16.26.1",
"stylelint-config-idiomatic-order": "^10.0.0",
"stylelint-config-recommended": "^17.0.0",
"stylelint-config-standard": "^39.0.1",
"stylelint-order": "^7.0.0",
"tslib": "^2.8.1",
"typescript": "^5.9.3"
},
"dependencies": {
"jsonpath-plus": "^10.3.0"

15
src/constants.ts Normal file
View file

@ -0,0 +1,15 @@
/**
*
*/
// 定时器间隔
export const MIN_DELAY_MS = 1000;
export const MS_PER_MINUTE = 60_000;
export const FALLBACK_CHECK_MS = 24 * 60 * 60 * 1000; // 24 小时
// 图片比例分析阈值
export const RATIO_CLOSE_THRESHOLD = 0.1; // 比例差异 < 10% 视为相近
export const RATIO_LARGE_DIFF_THRESHOLD = 0.5; // 比例差异 > 50% 视为差异很大
// 壁纸缓存
export const MAX_WALLPAPER_CACHE_SIZE = 100;

View file

@ -0,0 +1,266 @@
/**
*
*
*/
import { Notice, type Plugin } from "obsidian";
import { FALLBACK_CHECK_MS, MIN_DELAY_MS, MS_PER_MINUTE } from "../constants";
import { t } from "../i18n";
import type { BackgroundItem, DTBSettings } from "../types";
import { apiManager } from "../wallpaper-apis";
import type { EventBus } from "./event-bus";
import { logger } from "./logger";
import type { StyleManager } from "./style-manager";
import type { TimeRuleScheduler } from "./time-rule-scheduler";
export class BackgroundManager {
private scheduler: TimeRuleScheduler;
private styleManager: StyleManager;
private events: EventBus;
private plugin: Plugin;
// 状态
background: BackgroundItem | null = null;
private intervalId: number | null = null;
private timeoutId: number | null = null;
private lastActiveRuleId: string | null = null;
private isUpdating = false;
private onSettingsMutated: (() => void) | null = null;
constructor(
scheduler: TimeRuleScheduler,
styleManager: StyleManager,
events: EventBus,
plugin: Plugin,
onSettingsMutated?: () => void
) {
this.scheduler = scheduler;
this.styleManager = styleManager;
this.events = events;
this.plugin = plugin;
this.onSettingsMutated = onSettingsMutated ?? null;
}
/**
*
*/
stop(): void {
if (this.intervalId) {
clearInterval(this.intervalId);
this.intervalId = null;
}
if (this.timeoutId) {
clearTimeout(this.timeoutId);
this.timeoutId = null;
}
document.body.classList.remove("dtb-enabled");
logger.debug("Background manager stopped");
}
/**
*
*/
start(settings: DTBSettings): void {
this.stop();
if (!document.body.classList.contains("dtb-enabled")) {
document.body.classList.add("dtb-enabled");
}
// 立即执行一次更新
void this.update(settings, true);
if (settings.mode === "time-based") {
this.startTimeBasedMode(settings);
} else if (settings.mode === "interval") {
const intervalMs = settings.intervalMinutes * MS_PER_MINUTE;
this.intervalId = this.plugin.registerInterval(
window.setInterval(() => {
void this.update(settings, false);
}, intervalMs)
);
logger.debug("Background manager started (interval mode)", {
mode: settings.mode,
interval: intervalMs / 1000 + "s",
});
} else {
logger.debug("Background manager started (manual mode)", {
mode: settings.mode,
});
}
}
/**
*
*/
private startTimeBasedMode(settings: DTBSettings): void {
const scheduleNext = () => {
const nextRuleChange = this.scheduler.getNextChangeTime();
if (nextRuleChange) {
const delay = nextRuleChange - Date.now();
const actualDelay = Math.max(delay, MIN_DELAY_MS);
this.timeoutId = window.setTimeout(() => {
void this.update(settings, false);
// 只在活跃规则变化时才刷新 UI
const currentRule = this.scheduler.getCurrentRule();
const currentRuleId = currentRule?.id ?? null;
if (currentRuleId !== this.lastActiveRuleId) {
this.lastActiveRuleId = currentRuleId;
this.events.emit("time-rules:changed", {});
}
scheduleNext();
}, actualDelay);
logger.debug("Next background change scheduled", {
mode: settings.mode,
delay: Math.round(actualDelay / 1000) + "s",
nextTime: new Date(nextRuleChange).toLocaleTimeString(),
});
} else {
this.timeoutId = window.setTimeout(
() => {
void this.update(settings, false);
scheduleNext();
},
FALLBACK_CHECK_MS
);
logger.debug("No next rule found, checking again in 24 hours");
}
};
scheduleNext();
}
/**
*
*/
async update(settings: DTBSettings, forceUpdate = true): Promise<void> {
if (!settings.enabled) return;
// 防止并发更新
if (this.isUpdating) return;
this.isUpdating = true;
try {
let needsUpdate = false;
switch (settings.mode) {
case "time-based": {
const rule = this.scheduler.getCurrentRule();
if (rule) {
needsUpdate = this.background?.id !== rule.backgroundId;
if (needsUpdate) {
this.background =
settings.backgrounds.find((bg) => bg.id === rule.backgroundId) ?? null;
}
} else {
this.background = null;
needsUpdate = true;
}
logger.debug("TimeRule mode - current time rule", rule, needsUpdate);
break;
}
case "interval": {
const randomBg = await this.fetchRandomWallpaper(settings);
if (randomBg) {
this.background = randomBg;
needsUpdate = true;
} else if (settings.backgrounds.length > 0) {
settings.currentIndex =
(settings.currentIndex + 1) % settings.backgrounds.length;
this.background = settings.backgrounds[settings.currentIndex];
this.onSettingsMutated?.();
needsUpdate = true;
}
break;
}
default: {
this.background = settings.backgrounds[settings.currentIndex] ?? null;
}
}
if (forceUpdate || needsUpdate) {
this.styleManager.applyBackground(this.background, settings);
this.events.emit("backgrounds:changed", {});
}
} finally {
this.isUpdating = false;
}
}
/**
*
*/
async applyRandom(settings: DTBSettings): Promise<void> {
const bg = await this.fetchRandomWallpaper(settings);
if (bg) {
this.background = bg;
} else if (settings.backgrounds.length > 0) {
settings.currentIndex = (settings.currentIndex + 1) % settings.backgrounds.length;
this.background = settings.backgrounds[settings.currentIndex];
this.onSettingsMutated?.();
}
this.styleManager.applyBackground(this.background, settings);
this.events.emit("backgrounds:changed", {});
}
/**
* API
*/
private async fetchRandomWallpaper(settings: DTBSettings): Promise<BackgroundItem | null> {
if (!settings.enableRandomWallpaper) {
return null;
}
const enabledApis = apiManager.getEnabledApis();
if (enabledApis.length === 0) {
logger.warn("No enabled APIs found");
return null;
}
const selectedApi = enabledApis[Math.floor(Math.random() * enabledApis.length)];
try {
const loadingNotice = new Notice(
t("notice_api_fetching", { apiName: selectedApi.getName() }),
0
);
const wallpaperImages = await apiManager.getRandomWallpapers(selectedApi.getId());
loadingNotice.hide();
if (!wallpaperImages || wallpaperImages.length === 0) {
logger.warn(`No images returned from API: ${selectedApi.getName()}`);
return null;
}
const randomImage = wallpaperImages[Math.floor(Math.random() * wallpaperImages.length)];
if (randomImage?.url) {
new Notice(t("notice_api_success_applied", { apiName: selectedApi.getName() }));
return {
id: selectedApi.generateBackgroundId(),
name: selectedApi.generateBackgroundName(),
type: "image",
value: randomImage.url,
};
} else {
new Notice(t("notice_api_failed_fetch", { apiName: selectedApi.getName() }));
logger.warn(`No wallpaper image returned from API: ${selectedApi.getName()}`);
return null;
}
} catch (error) {
new Notice(
t("notice_api_error_fetch", {
apiName: selectedApi.getName(),
error: (error as Error).message,
})
);
logger.error("Error fetching random wallpaper:", error);
return null;
}
}
}

View file

@ -0,0 +1,88 @@
/**
*
* vault
*/
import { type App, Notice, requestUrl } from "obsidian";
import { t } from "../i18n";
import { confirm } from "../modals";
import type { BackgroundItem } from "../types";
import { logger } from "./logger";
export interface SaveResult {
success: true;
/** 更新后的 BackgroundItemvalue 替换为本地路径, remoteUrl 为备份) */
updatedBg: BackgroundItem;
}
export interface SaveError {
success: false;
error: string;
}
export class BackgroundPersistence {
constructor(private app: App) {}
/**
* vault
* BackgroundItem
*/
async saveRemoteImage(bg: BackgroundItem, folderPath: string): Promise<SaveResult | SaveError> {
if (!folderPath) {
return { success: false, error: "no_folder_path" };
}
// 规范化文件名
const imageName = bg.name.replace(/[\\/:*?"<>|]/g, "_") + ".jpg";
const localPath = `${folderPath}/${imageName}`;
// 判断路径是否存在
const file = this.app.vault.getFileByPath(localPath);
if (file) {
const overwrite = await confirm(
this.app,
t("notice_save_background_overwrite_existing_file", { filePath: localPath })
);
if (!overwrite) {
return { success: false, error: "cancelled" };
}
}
// 下载远程图片
try {
const response = await requestUrl({ url: bg.value });
if (response.status < 200 || response.status >= 300) {
new Notice(t("notice_save_background_failed"), response.status);
return { success: false, error: `HTTP ${response.status}` };
}
const arrayBuffer = response.arrayBuffer;
// 覆盖旧文件
if (file) {
await this.app.fileManager.trashFile(file);
}
await this.app.vault.createBinary(localPath, arrayBuffer);
// 创建新的 BackgroundItem不修改原对象
const updatedBg: BackgroundItem = {
...bg,
remoteUrl: bg.value,
value: localPath,
};
new Notice(
t("notice_save_background_converted", {
oldPath: bg.value,
newPath: localPath,
}),
5000
);
return { success: true, updatedBg };
} catch (error) {
logger.error("Error saving remote image:", error);
return { success: false, error: String(error) };
}
}
}

92
src/core/event-bus.ts Normal file
View file

@ -0,0 +1,92 @@
/**
* 线
* ApiStateManager workspace
*/
import type { BackgroundItem, DTBSettings } from "../types";
/**
* API
*/
export interface ApiStateInfo {
configEnabled: boolean;
instanceEnabled: boolean;
isLoading: boolean;
error?: string;
}
/**
*
*/
export interface DTBEventMap {
"background:changed": { background: BackgroundItem | null };
"settings:changed": { key: keyof DTBSettings; value: unknown };
"api:state-changed": { apiId: string; state: ApiStateInfo };
"time-rules:changed": Record<string, never>;
"backgrounds:changed": Record<string, never>;
"apis:changed": Record<string, never>;
"css:updated": Record<string, never>;
}
export type DTBEventKey = keyof DTBEventMap;
type Listener<T> = (payload: T) => void;
/**
* 线
*/
export class EventBus {
private listeners = new Map<string, Set<Listener<unknown>>>();
/**
*
*/
on<K extends DTBEventKey>(event: K, cb: Listener<DTBEventMap[K]>): () => void {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set());
}
const set = this.listeners.get(event)!;
set.add(cb as Listener<unknown>);
return () => {
set.delete(cb as Listener<unknown>);
if (set.size === 0) {
this.listeners.delete(event);
}
};
}
/**
*
*/
emit<K extends DTBEventKey>(event: K, payload: DTBEventMap[K]): void {
const set = this.listeners.get(event);
if (!set) return;
for (const cb of [...set]) {
try {
cb(payload);
} catch (error) {
console.error(`DTB EventBus: error in listener for "${event}":`, error);
}
}
}
/**
*
*/
off<K extends DTBEventKey>(event: K, cb: Listener<DTBEventMap[K]>): void {
const set = this.listeners.get(event);
if (!set) return;
set.delete(cb as Listener<unknown>);
if (set.size === 0) {
this.listeners.delete(event);
}
}
/**
*
*/
removeAllListeners(): void {
this.listeners.clear();
}
}

8
src/core/index.ts Normal file
View file

@ -0,0 +1,8 @@
export { BackgroundManager } from "./background-manager";
export { BackgroundPersistence } from "./background-persistence";
export type { SaveError, SaveResult } from "./background-persistence";
export { EventBus } from "./event-bus";
export type { ApiStateInfo, DTBEventKey, DTBEventMap } from "./event-bus";
export { logger } from "./logger";
export { StyleManager } from "./style-manager";
export { TimeRuleScheduler } from "./time-rule-scheduler";

12
src/core/logger.ts Normal file
View file

@ -0,0 +1,12 @@
/**
*
* console.debug/warn/error "DTB"
*/
const PREFIX = "DTB";
export const logger = {
debug: (msg: string, ...args: unknown[]) => console.debug(`${PREFIX}: ${msg}`, ...args),
warn: (msg: string, ...args: unknown[]) => console.warn(`${PREFIX}: ${msg}`, ...args),
error: (msg: string, ...args: unknown[]) => console.error(`${PREFIX}: ${msg}`, ...args),
};

200
src/core/style-manager.ts Normal file
View file

@ -0,0 +1,200 @@
/**
*
* CSS URL
*/
import type { App } from "obsidian";
import { RATIO_CLOSE_THRESHOLD, RATIO_LARGE_DIFF_THRESHOLD } from "../constants";
import type { BackgroundItem, DTBSettings } from "../types";
import { hexToRgba } from "../utils";
import { logger } from "./logger";
export class StyleManager {
private app: App;
private imageSizeCache = new Map<string, string>();
constructor(app: App) {
this.app = app;
}
/**
* CSS
*/
applyBackground(bg: BackgroundItem | null, settings: DTBSettings, bgSize?: string): void {
if (!settings.enabled) {
return;
}
if (!bg) {
// 没有激活背景时,仍然更新遮罩变量
const bgColorLight = hexToRgba(settings.bgColorLight, settings.bgColorOpacityLight);
const bgColorDark = hexToRgba(settings.bgColorDark, settings.bgColorOpacityDark);
document.documentElement.setCssProps({
"--dtb-bg-image": "none",
"--dtb-bg-color-light": bgColorLight,
"--dtb-bg-color-dark": bgColorDark,
});
} else {
const bgCssValue = bg.type === "image" ? this.getBgURL(bg) : bg.value;
// 优先级: 传入的自定义值 > 背景单独的设置 > 全局默认设置
const blurDepth = bg.blurDepth ?? settings.blurDepth;
const brightness4Bg = bg.brightness4Bg ?? settings.brightness4Bg;
const saturate4Bg = bg.saturate4Bg ?? settings.saturate4Bg;
// 遮罩颜色与透明度
const lightColor = bg.bgColorLight ?? settings.bgColorLight;
const lightOpacity = bg.bgColorOpacityLight ?? settings.bgColorOpacityLight;
const darkColor = bg.bgColorDark ?? settings.bgColorDark;
const darkOpacity = bg.bgColorOpacityDark ?? settings.bgColorOpacityDark;
const bgColorLight = hexToRgba(lightColor, lightOpacity);
const bgColorDark = hexToRgba(darkColor, darkOpacity);
bgSize = bgSize ?? bg.bgSize ?? settings.bgSize ?? "intelligent";
// intelligent 模式下动态选择
if (bgSize === "intelligent") {
if (bg.type === "image") {
bgSize = this.getOptimalBackgroundSize(bg.value);
} else {
bgSize = "auto";
}
}
document.documentElement.setCssProps({
"--dtb-bg-image": bgCssValue,
"--dtb-blur-depth": `${blurDepth}px`,
"--dtb-brightness": `${brightness4Bg}`,
"--dtb-saturate": `${saturate4Bg}`,
"--dtb-bg-color-light": bgColorLight,
"--dtb-bg-color-dark": bgColorDark,
"--dtb-bg-size": bgSize,
});
}
// 通知 css-change
this.app.workspace.trigger("css-change", { source: "dtb" });
}
/**
* CSS URL
* remoteUrl URL
*/
getBgURL(bg: BackgroundItem): string {
const imagePath = bg.value;
if (this.isRemoteImage(imagePath)) {
return `url("${imagePath}")`;
}
// 本地图片路径
const file = this.app.vault.getFileByPath(imagePath);
if (file) {
const p = this.app.vault.getResourcePath(file);
if (p) {
return `url("${p}")`;
} else {
logger.warn(`Resource path for ${imagePath} is empty`);
}
} else {
logger.warn(`Image ${imagePath} not found or inaccessible`);
}
// 回退到 remoteUrl 备份(不修改 bg 对象,仅使用远端 URL 渲染)
if (bg.remoteUrl) {
logger.warn(`Local path invalid, falling back to remote URL for "${bg.name}"`);
return `url("${bg.remoteUrl}")`;
}
return "none";
}
isRemoteImage(imagePath: string): boolean {
return imagePath.startsWith("http://") || imagePath.startsWith("https://");
}
/**
* background-size
*/
private getOptimalBackgroundSize(imagePath: string): string {
if (this.isRemoteImage(imagePath)) {
return "contain";
}
// 检查缓存
const cached = this.imageSizeCache.get(imagePath);
if (cached) {
return cached;
}
try {
const screenRatio = window.innerWidth / window.innerHeight;
const file = this.app.vault.getFileByPath(imagePath);
if (!file) return "contain";
const resourcePath = this.app.vault.getResourcePath(file);
if (!resourcePath) return "contain";
// 异步分析图片尺寸
void this.loadImageAndAnalyze(resourcePath, screenRatio, imagePath);
return "contain"; // 默认返回 contain异步更新后会重新渲染
} catch (error) {
logger.warn("Error determining optimal background size:", error);
return "contain";
}
}
/**
*
*
*/
private async loadImageAndAnalyze(
resourcePath: string,
screenRatio: number,
imagePath: string
): Promise<string> {
try {
const img = new Image();
await new Promise<void>((resolve, reject) => {
img.onload = () => resolve();
img.onerror = () => reject(new Error("Failed to load image"));
img.src = resourcePath;
});
const imageRatio = img.naturalWidth / img.naturalHeight;
const ratioDifference = Math.abs(imageRatio - screenRatio) / screenRatio;
let optimalSize: string;
if (ratioDifference < RATIO_CLOSE_THRESHOLD) {
optimalSize = "cover";
} else if (imageRatio > screenRatio) {
optimalSize = "contain";
} else {
optimalSize =
ratioDifference > RATIO_LARGE_DIFF_THRESHOLD ? "contain" : "cover";
}
logger.debug(
`Image analysis - Screen: ${screenRatio.toFixed(2)}, Image: ${imageRatio.toFixed(2)}, Size: ${optimalSize}`
);
// 缓存结果
this.imageSizeCache.set(imagePath, optimalSize);
// 限制缓存大小
if (this.imageSizeCache.size > 50) {
const firstKey = this.imageSizeCache.keys().next().value;
if (firstKey) this.imageSizeCache.delete(firstKey);
}
return optimalSize;
} catch (error) {
logger.warn("Error loading image for size calculation:", error);
return "contain";
}
}
/**
* resize
*/
clearImageSizeCache(): void {
this.imageSizeCache.clear();
}
}

View file

@ -0,0 +1,131 @@
/**
*
*
*/
import type { TimeRule } from "../types";
export class TimeRuleScheduler {
private rules: TimeRule[] = [];
constructor(rules: TimeRule[]) {
this.rules = rules;
}
updateRules(rules: TimeRule[]): void {
this.rules = rules;
}
/**
*
*
*/
getCurrentRule(now?: Date): TimeRule | null {
now = now ?? new Date();
const currentTimeMinutes = now.getHours() * 60 + now.getMinutes();
// 按 startTime 排序,优先匹配靠前的规则
const sortedRules = this.rules
.filter((rule) => rule.enabled)
.map((rule) => {
const { startTime, endTime } = this.parseTimeRule(rule);
return { rule, startTime, endTime };
})
.sort((a, b) => a.startTime - b.startTime);
// 遍历排序后的规则,找到第一个匹配的
for (const { rule, startTime, endTime } of sortedRules) {
// 跨天时段处理:如 20:00-06:00
if (startTime > endTime) {
if (currentTimeMinutes >= startTime || currentTimeMinutes < endTime) {
return rule;
}
} else {
if (currentTimeMinutes >= startTime && currentTimeMinutes < endTime) {
return rule;
}
}
}
return null;
}
/**
*
*/
getNextChangeTime(now?: Date): number | null {
const enabledRules = this.rules.filter((r) => r.enabled);
if (enabledRules.length === 0) {
return null;
}
now = now ?? new Date();
const currentTimeMinutes = now.getHours() * 60 + now.getMinutes();
// 收集所有启用的时段规则的开始和结束时间点
const timePoints: Array<{ time: number; isStart: boolean; rule: TimeRule }> = [];
for (const rule of this.rules) {
if (!rule.enabled) continue;
const { startTime, endTime } = this.parseTimeRule(rule);
timePoints.push({ time: startTime, isStart: true, rule });
timePoints.push({ time: endTime, isStart: false, rule });
}
// 按时间排序
timePoints.sort((a, b) => a.time - b.time);
// 查找下一个时间点
let nextPoint = null;
// 首先查找今天剩余时间内的下一个时间点
for (const point of timePoints) {
if (point.time > currentTimeMinutes) {
nextPoint = point;
break;
}
}
// 如果今天没有找到,取明天的第一个时间点
if (!nextPoint && timePoints.length > 0) {
nextPoint = timePoints[0];
}
if (!nextPoint) {
return null;
}
// 计算具体的时间戳
const targetDate = new Date(now);
const targetHours = Math.floor(nextPoint.time / 60);
const targetMinutes = nextPoint.time % 60;
targetDate.setHours(targetHours, targetMinutes, 0, 0);
// 如果目标时间已经过了,说明是明天的时间点
if (targetDate.getTime() <= now.getTime()) {
targetDate.setDate(targetDate.getDate() + 1);
}
return targetDate.getTime();
}
/**
*
*/
parseTimeRule(rule: TimeRule): { startTime: number; endTime: number } {
const [startHour, startMin] = rule.startTime.split(":").map(Number);
const [endHour, endMin] = rule.endTime.split(":").map(Number);
// 防御性检查:格式异常时返回 0:00-0:00等价于禁用
if (isNaN(startHour) || isNaN(startMin) || isNaN(endHour) || isNaN(endMin)) {
return { startTime: 0, endTime: 0 };
}
const startTime = startHour * 60 + startMin;
const endTime = endHour * 60 + endMin;
return { startTime, endTime };
}
}

View file

@ -1,7 +1,7 @@
import { App, Modal, Setting } from "obsidian";
import { t } from "../i18n";
import type DynamicThemeBackgroundPlugin from "../plugin";
import { BackgroundItem } from "../types";
import type { BackgroundItem, DTBSettings } from "../types";
import { addDropdownOptionHoverTooltip } from "../utils";
import { ImagePathSuggestModal } from "./image-path-suggest-modal";
@ -17,8 +17,8 @@ export class BackgroundModal extends Modal {
bgItem: BackgroundItem;
onSubmit: (bg: BackgroundItem) => void;
nameInput: HTMLInputElement;
valueInput: HTMLInputElement;
nameInput!: HTMLInputElement;
valueInput!: HTMLInputElement;
// 背景单独的模糊度、亮度、饱和度、遮罩颜色和透明度、填充方式设置
blurDepth?: number;
brightness4Bg?: number;
@ -67,6 +67,7 @@ export class BackgroundModal extends Modal {
// Name input
contentEl.createEl("label", { text: t("bg_name_label") });
this.nameInput = contentEl.createEl("input", { type: "text", cls: "dtb-input" });
this.nameInput.value = this.bgItem.name;
// Value input
let valueLabel = "";
@ -96,6 +97,7 @@ export class BackgroundModal extends Modal {
placeholder,
cls: "dtb-flex-1",
});
this.valueInput.value = this.bgItem.value;
const browseButton = inputContainer.createEl("button", {
type: "button",
text: t("button_browse"),
@ -112,6 +114,7 @@ export class BackgroundModal extends Modal {
placeholder,
cls: "dtb-input",
});
this.valueInput.value = this.bgItem.value;
}
// 背景单独的模糊度、亮度、饱和度、遮罩颜色和透明度、填充方式设置
@ -318,8 +321,8 @@ export class BackgroundModal extends Modal {
);
dropdown
.setValue(this.bgSize ?? this.bgItem.bgSize ?? this.plugin.settings.bgSize)
.onChange((value: "cover" | "contain" | "auto" | "intelligent") => {
this.bgSize = value;
.onChange((value: string) => {
this.bgSize = value as DTBSettings["bgSize"];
});
return dropdown;

View file

@ -5,9 +5,9 @@ import type { TimeRule } from "../types";
export class TimeRuleModal extends Modal {
rule: TimeRule;
onSubmit: (rule: { name: string; startTime: string; endTime: string }) => void;
nameInput: HTMLInputElement;
startTimeInput: HTMLInputElement;
endTimeInput: HTMLInputElement;
nameInput!: HTMLInputElement;
startTimeInput!: HTMLInputElement;
endTimeInput!: HTMLInputElement;
constructor(
app: App,

View file

@ -1,4 +1,6 @@
import { App, Modal, Notice } from "obsidian";
import { logger } from "../core/logger";
import { generateId } from "../utils/utils";
import { t } from "../i18n";
import {
apiManager,
@ -20,23 +22,23 @@ export class WallpaperApiEditorModal extends Modal {
onSubmit: (apiConfig: WallpaperApiConfig) => void;
// 基础配置输入元素
nameInput: HTMLInputElement;
descInput: HTMLInputElement; // 可选描述输入
typeSelect: HTMLSelectElement;
urlInput: HTMLInputElement;
nameInput!: HTMLInputElement;
descInput!: HTMLInputElement; // 可选描述输入
typeSelect!: HTMLSelectElement;
urlInput!: HTMLInputElement;
// Headers配置
headersContainer: HTMLDivElement;
headersContainer!: HTMLDivElement;
headerInputs: Array<{ key: HTMLInputElement; value: HTMLInputElement }> = [];
// 参数配置容器的引用
paramsSectionContainer: HTMLElement;
paramsSectionContainer!: HTMLElement;
// 动态参数输入元素映射
paramInputs: Map<string, HTMLElement> = new Map();
// 额外参数输入
extraParamsTextarea: HTMLTextAreaElement;
extraParamsTextarea!: HTMLTextAreaElement;
// 自定义设置容器的引用
customSettingsSectionContainer: HTMLElement;
customSettingsSectionContainer!: HTMLElement;
// 自定义设置输入元素
customSettingsInputs: Map<string, HTMLElement> = new Map();
@ -400,7 +402,7 @@ export class WallpaperApiEditorModal extends Modal {
try {
currentValue = descriptor.fromApiValue(currentValue as ApiValueType);
} catch (error) {
console.warn(`Failed to convert value for ${descriptor.key}:`, error);
logger.warn(`Failed to convert value for ${descriptor.key}:`, error);
currentValue = descriptor.defaultValue;
}
}
@ -620,7 +622,7 @@ export class WallpaperApiEditorModal extends Modal {
});
return {
id: this.apiConfig.id || `api-${Date.now()}`,
id: this.apiConfig.id || generateId("api"),
name: this.nameInput.value || t("api_modal_unnamed_api"),
description: this.descInput.value || "",
type: this.typeSelect.value as WallpaperApiType,
@ -718,14 +720,15 @@ export class WallpaperApiEditorModal extends Modal {
return;
}
// 这里需临时创建个实例来测试
await apiManager.createApi(config);
// 如果能成功启用则测试通过
await apiManager.enableApi(config.id);
// 使用临时 ID 避免覆盖同 ID 的已有实例
const testConfig = { ...config, id: generateId("api-test") };
await apiManager.createApi(testConfig);
await apiManager.enableApi(testConfig.id);
new Notice(t("api_modal_test_successful"));
apiManager.deleteApi(config.id); // 测试后删除临时实例
apiManager.deleteApi(testConfig.id);
} catch (error) {
new Notice(t("api_modal_test_failed", { error: (error as Error).message }));
}

View file

@ -1,28 +1,30 @@
/**
* -
*
*/
import { Notice, Plugin, requestUrl } from "obsidian";
import { Notice, Plugin } from "obsidian";
import { registerCommands } from "./commands";
import { BackgroundManager, BackgroundPersistence, EventBus, StyleManager, TimeRuleScheduler, logger } from "./core";
import { getDefaultSettings } from "./default-settings";
import { t } from "./i18n";
import { confirm } from "./modals";
import { DTBSettingTab, DTBSettingsView, DTB_SETTINGS_VIEW_TYPE } from "./settings";
import type { BackgroundItem, DTBSettings, TimeRule } from "./types";
import { hexToRgba } from "./utils";
import type { BackgroundItem, DTBSettings } from "./types";
import { apiManager } from "./wallpaper-apis";
export default class DynamicThemeBackgroundPlugin extends Plugin {
settings: DTBSettings;
settings!: DTBSettings;
events: EventBus = new EventBus();
// 内部状态
background: BackgroundItem | null = null; // 当前的背景
intervalId: number | null = null; // 用于间隔模式的定时器 ID
timeoutId: number | null = null; // 用于时段规则的定时器 ID
// 核心服务
private scheduler!: TimeRuleScheduler;
styleManager!: StyleManager;
private persistence!: BackgroundPersistence;
bgManager!: BackgroundManager;
// 界面元素
statusBar: HTMLElement | null = null; // 状态栏
settingTabs: Map<string, DTBSettingTab> = new Map(); // 存储所有设置标签页的引用
statusBar: HTMLElement | null = null;
settingTabs: Map<string, DTBSettingTab> = new Map();
// ============================================================================
// 主要接口方法
@ -31,10 +33,13 @@ export default class DynamicThemeBackgroundPlugin extends Plugin {
async onload() {
await this.loadSettings();
// 左侧栏图标
// this.addRibbonIcon("rainbow", "🌈 DTB", async (evt: MouseEvent) => {
// await this.applyRandomWallpaper();
// });
// 初始化核心服务
this.scheduler = new TimeRuleScheduler(this.settings.timeRules);
this.styleManager = new StyleManager(this.app);
this.persistence = new BackgroundPersistence(this.app);
this.bgManager = new BackgroundManager(this.scheduler, this.styleManager, this.events, this, () => {
void this.saveSettings();
});
// 状态栏
if (this.settings.statusBarEnabled) {
@ -52,28 +57,24 @@ export default class DynamicThemeBackgroundPlugin extends Plugin {
// 启动背景管理器
if (this.settings.enabled) {
// 等待 layout-ready 事件,确保 vault 完全加载
this.app.workspace.onLayoutReady(() => {
this.startBackgroundManager();
});
}
// Wallpaper API 管理器
// 实例化所有已配置的API
for (const apiConfig of this.settings.wallpaperApis) {
void apiManager.createApi(apiConfig);
}
console.debug("Dynamic Theme Background plugin loaded");
logger.debug("Plugin loaded");
}
onunload() {
this.stopBackgroundManager();
// 清理所有注册的API实例包括状态管理器中的所有订阅
apiManager.deleteAllApis();
console.debug("Dynamic Theme Background plugin unloaded");
this.events.removeAllListeners();
logger.debug("Plugin unloaded");
}
async loadSettings() {
@ -86,39 +87,144 @@ export default class DynamicThemeBackgroundPlugin extends Plugin {
await this.saveData(this.settings);
}
/**
*
*/
activateView(): void {
// 检查是否已经存在该类型的 leaf
const existingLeaf = this.app.workspace.getLeavesOfType(DTB_SETTINGS_VIEW_TYPE)[0];
// ============================================================================
// 背景管理器代理
// ============================================================================
get background(): BackgroundItem | null {
return this.bgManager?.background ?? null;
}
set background(bg: BackgroundItem | null) {
if (this.bgManager) {
this.bgManager.background = bg;
}
}
startBackgroundManager() {
this.scheduler.updateRules(this.settings.timeRules);
this.bgManager.start(this.settings);
}
stopBackgroundManager() {
this.bgManager?.stop();
}
/**
*
*/
async updateBackground(forceUpdate = true) {
await this.bgManager.update(this.settings, forceUpdate);
}
/**
* CSS StyleManager
*/
updateStyleCss(bgSize?: string) {
this.styleManager.applyBackground(this.background, this.settings, bgSize);
}
/**
*
*/
async applyRandomWallpaper(): Promise<void> {
await this.bgManager.applyRandom(this.settings);
this.refreshActiveBackgrounds();
}
// ============================================================================
// 背景保存
// ============================================================================
/**
*
*/
async saveBackground(bg: BackgroundItem | null = this.background) {
if (!bg) return;
if (!this.settings.localBackgroundFolder) {
new Notice(t("notice_save_background_valid_folder_path_required"));
return;
}
if (bg.type !== "image") {
new Notice(t("notice_save_background_only_image_supported"));
return;
}
if (!this.styleManager.isRemoteImage(bg.value)) {
new Notice(t("notice_save_background_no_need_save_local"));
return;
}
const result = await this.persistence.saveRemoteImage(bg, this.settings.localBackgroundFolder);
if (!result.success) {
if (result.error !== "cancelled") {
new Notice(t("notice_save_background_failed"));
}
return;
}
// 更新 settings 中的 bg 引用(不可变更新)
const updatedBg = result.updatedBg;
const bgIndex = this.settings.backgrounds.findIndex((item) => item.id === updatedBg.id);
if (bgIndex >= 0) {
this.settings.backgrounds[bgIndex] = updatedBg;
} else {
this.settings.backgrounds.push(updatedBg);
this.refreshActiveBackgrounds();
this.refreshActiveTimeRules();
}
// 更新当前显示的背景
if (this.background?.id === updatedBg.id) {
this.bgManager.background = updatedBg;
}
await this.saveSettings();
new Notice(t("notice_save_background_success", { folderPath: this.settings.localBackgroundFolder }));
}
// ============================================================================
// 工具方法(代理)
// ============================================================================
getBgURL(bg: BackgroundItem): string {
return this.styleManager.getBgURL(bg);
}
isRemoteImage(imagePath: string): boolean {
return this.styleManager.isRemoteImage(imagePath);
}
getCurrentTimeRule() {
return this.scheduler.getCurrentRule();
}
getNextRuleChangeTime() {
return this.scheduler.getNextChangeTime();
}
// ============================================================================
// 界面方法
// ============================================================================
activateView(): void {
const existingLeaf = this.app.workspace.getLeavesOfType(DTB_SETTINGS_VIEW_TYPE)[0];
if (existingLeaf) {
// 如果已存在,则聚焦它
void this.app.workspace.revealLeaf(existingLeaf);
} else {
// 如果不存在,则创建新的 leaf
const leaf = this.app.workspace.getLeaf("tab");
void leaf.setViewState({
type: DTB_SETTINGS_VIEW_TYPE,
active: true,
});
// 确保标签页获得焦点
void this.app.workspace.revealLeaf(leaf);
}
}
/**
*
*/
deactivateStatusBar() {
this.statusBar?.empty();
this.statusBar?.remove();
this.statusBar = null;
}
/**
*
*/
activateStatusBar() {
this.deactivateStatusBar();
this.statusBar = this.addStatusBarItem();
@ -141,661 +247,43 @@ export default class DynamicThemeBackgroundPlugin extends Plugin {
});
}
/**
*
*/
stopBackgroundManager() {
if (this.intervalId) {
clearInterval(this.intervalId);
this.intervalId = null;
}
if (this.timeoutId) {
clearTimeout(this.timeoutId);
this.timeoutId = null;
}
document.body.classList.remove("dtb-enabled");
console.debug("DTB: Background manager stopped");
}
/**
*
*/
startBackgroundManager() {
this.stopBackgroundManager();
// 如果没有 'dtb-enabled' 类,则添加
if (!document.body.classList.contains("dtb-enabled")) {
document.body.classList.add("dtb-enabled");
}
// 立即执行一次更新
void this.updateBackground(true);
if (this.settings.mode === "time-based") {
// 时段模式:使用 setTimeout计算到下一个时段的时间
this.startTimeBasedManager();
} else if (this.settings.mode === "interval") {
// 间隔模式:使用 setInterval
const intervalMs = this.settings.intervalMinutes * 60000;
this.intervalId = this.registerInterval(
window.setInterval(() => {
void this.updateBackground(false);
}, intervalMs)
);
console.debug("DTB: Background manager started (interval mode)", {
mode: this.settings.mode,
interval: intervalMs / 1000 + "s",
});
} else {
console.debug("DTB: Background manager started (manual mode)", {
mode: this.settings.mode,
});
}
}
// 时段规则下的背景更新循环,通过 setTimeout 实现
startTimeBasedManager(): void {
const scheduleNext = () => {
const nextRuleChange = this.getNextRuleChangeTime();
if (nextRuleChange) {
const delay = nextRuleChange - Date.now();
// 确保延迟时间为正数最少1秒
const actualDelay = Math.max(delay, 1000);
this.timeoutId = window.setTimeout(() => {
void this.updateBackground(false);
// 此处应该刷新设置页中的时间规则列表
this.refreshActiveTimeRules();
scheduleNext(); // 递归调度下一个时段
}, actualDelay);
console.debug("DTB: Next background change scheduled", {
mode: this.settings.mode,
delay: Math.round(actualDelay / 1000) + "s",
nextTime: new Date(nextRuleChange).toLocaleTimeString(),
});
} else {
// 如果没有下一个时段24小时后重新检查
this.timeoutId = window.setTimeout(
() => {
void this.updateBackground(false);
scheduleNext();
},
24 * 60 * 60 * 1000
);
console.debug("DTB: No next rule found, checking again in 24 hours");
}
};
scheduleNext();
}
/**
*
* @param forceUpdate
*/
async updateBackground(forceUpdate = true) {
if (!this.settings.enabled) return;
let needsUpdate = false;
switch (this.settings.mode) {
// 基于时段切换
case "time-based": {
const rule = this.getCurrentTimeRule();
if (rule) {
// 判断是否与当前背景不同
needsUpdate = this.background?.id !== rule.backgroundId;
if (needsUpdate) {
this.background = this.settings.backgrounds.find((bg) => bg.id === rule.backgroundId) ?? null;
}
} else {
// 当前没有匹配的时段规则,取消背景
this.background = null;
needsUpdate = true; // 强制更新样式以清除背景
}
// 调试信息, 降低等级,避免刷屏
console.debug("DTB: TimeRule mode - current time rule", rule, needsUpdate);
break;
}
// 基于时间间隔切换
case "interval": {
const randomBg = await this.fetchRandomWallpaperFromAPI();
if (randomBg) {
this.background = randomBg;
needsUpdate = true;
} else if (this.settings.backgrounds.length > 0) {
// API失败时回退到本地背景
this.settings.currentIndex = (this.settings.currentIndex + 1) % this.settings.backgrounds.length;
this.background = this.settings.backgrounds[this.settings.currentIndex];
void this.saveSettings();
needsUpdate = true;
}
break;
}
default: {
// 手动模式
this.background = this.settings.backgrounds[this.settings.currentIndex];
}
}
if (forceUpdate || needsUpdate) {
this.updateStyleCss();
// 这里需要刷新设置页面,以正确显示激活图标
this.refreshActiveBackgrounds();
}
}
/**
* CSS, this.background
* @param bgSize
*/
updateStyleCss(bgSize?: string) {
if (!this.settings.enabled) {
return;
}
if (!this.background) {
// 没有激活背景时,仍然更新遮罩变量,保证设置页调整立即生效
const bgColorLight = hexToRgba(this.settings.bgColorLight, this.settings.bgColorOpacityLight);
const bgColorDark = hexToRgba(this.settings.bgColorDark, this.settings.bgColorOpacityDark);
document.documentElement.setCssProps({
"--dtb-bg-image": "none",
"--dtb-bg-color-light": bgColorLight,
"--dtb-bg-color-dark": bgColorDark,
});
} else {
const bgCssValue =
this.background.type === "image" ? this.getBgURL(this.background) : this.background.value;
// 模糊度、亮度、饱和度、遮罩颜色和透明度、填充方式的优先级统一为:
// 传入的自定义值 > 背景单独的设置 > 全局默认设置
const blurDepth = this.background.blurDepth ?? this.settings.blurDepth;
const brightness4Bg = this.background.brightness4Bg ?? this.settings.brightness4Bg;
const saturate4Bg = this.background.saturate4Bg ?? this.settings.saturate4Bg;
// 计算遮罩颜色与透明度(优先级:每背景按主题覆盖 > 全局 light/dark
const perBgLightColor = this.background.bgColorLight;
const perBgLightOpacity = this.background.bgColorOpacityLight;
const perBgDarkColor = this.background.bgColorDark;
const perBgDarkOpacity = this.background.bgColorOpacityDark;
const lightColor = perBgLightColor ?? this.settings.bgColorLight;
const lightOpacity = perBgLightOpacity ?? this.settings.bgColorOpacityLight;
const darkColor = perBgDarkColor ?? this.settings.bgColorDark;
const darkOpacity = perBgDarkOpacity ?? this.settings.bgColorOpacityDark;
const bgColorLight = hexToRgba(lightColor, lightOpacity);
const bgColorDark = hexToRgba(darkColor, darkOpacity);
bgSize = bgSize ?? this.background.bgSize ?? this.settings.bgSize ?? "intelligent";
// 如果是 "intelligent",则根据图片和屏幕比例动态选择
if (bgSize === "intelligent") {
if (this.background.type === "image") {
bgSize = this.getOptimalBackgroundSize(this.background.value);
} else {
bgSize = "auto"; // 对于非图片背景,使用 auto
}
}
// 通过修改根元素的 CSS 属性来更新背景样式
document.documentElement.setCssProps({
"--dtb-bg-image": bgCssValue,
"--dtb-blur-depth": `${blurDepth}px`,
"--dtb-brightness": `${brightness4Bg}`,
"--dtb-saturate": `${saturate4Bg}`,
// 仅设置明/暗两套变量;统一变量 --dtb-bg-color 交由 CSS 中的 .theme-light/.theme-dark 进行映射
"--dtb-bg-color-light": bgColorLight,
"--dtb-bg-color-dark": bgColorDark,
"--dtb-bg-size": bgSize,
});
}
// 通知 css-change
this.app.workspace.trigger("css-change", { source: "dtb" });
}
// ============================================================================
// 辅助方法
// 设置页刷新(保持兼容,后续 R3 将替换为纯事件驱动)
// ============================================================================
/**
* background-size
* @param imagePath
* @returns background-size值
*/
private getOptimalBackgroundSize(imagePath: string): string {
// 对于远程图片由于无法同步获取尺寸使用contain作为默认值
if (imagePath.startsWith("http://") || imagePath.startsWith("https://")) {
return "contain";
}
try {
// 获取屏幕比例
const screenRatio = window.innerWidth / window.innerHeight;
// 对于本地图片,尝试获取图片尺寸
const file = this.app.vault.getFileByPath(imagePath);
if (!file) {
return "contain";
}
// 获取资源路径
const resourcePath = this.app.vault.getResourcePath(file);
if (!resourcePath) {
return "contain";
}
// 异步加载图片并更新尺寸
void this.loadImageAndUpdateSize(resourcePath, screenRatio);
return "contain"; // 默认返回contain异步更新后会重新渲染
} catch (error) {
console.warn("DTB: Error determining optimal background size:", error);
return "contain";
}
}
/**
* background-size
* @param resourcePath
* @param screenRatio
*/
private async loadImageAndUpdateSize(resourcePath: string, screenRatio: number) {
try {
const img = new Image();
await new Promise<void>((resolve, reject) => {
img.onload = () => resolve();
img.onerror = () => reject(new Error("Failed to load image"));
img.src = resourcePath;
});
const imageRatio = img.naturalWidth / img.naturalHeight;
// 计算比例差异
const ratioDifference = Math.abs(imageRatio - screenRatio) / screenRatio;
let optimalSize: string;
if (ratioDifference < 0.1) {
// 比例相近差异小于10%使用cover以获得最佳填充效果
optimalSize = "cover";
} else if (imageRatio > screenRatio) {
// 图片更宽使用contain以显示完整宽度
optimalSize = "contain";
} else {
// 图片更高,根据差异程度选择
if (ratioDifference > 0.5) {
// 差异很大使用contain以确保完整显示
optimalSize = "contain";
} else {
// 差异适中使用cover以获得更好的视觉效果
optimalSize = "cover";
}
}
console.debug(
`DTB: Image analysis - Screen ratio: ${screenRatio.toFixed(2)}, Image ratio: ${imageRatio.toFixed(
2
)}, Optimal size: ${optimalSize}`
);
// 使用优化后的方法更新样式(如果背景仍然是当前图片)
if (this.background && this.background.type === "image") {
this.updateStyleCss(optimalSize);
}
} catch (error) {
console.warn("DTB: Error loading image for size calculation:", error);
}
}
/**
*
*
* API URL
* API 退
* CSS
*
*/
async applyRandomWallpaper(): Promise<void> {
const bg = await this.fetchRandomWallpaperFromAPI();
if (bg) {
this.background = bg;
} else if (this.settings.backgrounds.length > 0) {
// API失败时回退到本地背景
this.settings.currentIndex = (this.settings.currentIndex + 1) % this.settings.backgrounds.length;
this.background = this.settings.backgrounds[this.settings.currentIndex];
await this.saveSettings();
}
this.updateStyleCss();
// 这里需要刷新设置页面
this.refreshActiveBackgrounds();
}
/**
* ,
*/
async saveBackground(bg: BackgroundItem | null = this.background) {
if (!bg) return;
// 判断是否设置了 localBackgroundFolder
if (!this.settings.localBackgroundFolder) {
new Notice(t("notice_save_background_valid_folder_path_required"));
return;
}
// 判断是否是图片,非图片则不保存
if (bg.type !== "image") {
new Notice(t("notice_save_background_only_image_supported"));
return;
}
// 如果是本地图片,则不保存
if (!this.isRemoteImage(bg.value)) {
new Notice(t("notice_save_background_no_need_save_local"));
return;
}
// 保存远程图片
const success = await this.saveRemoteImage(bg, this.settings.localBackgroundFolder);
if (!success) {
new Notice(t("notice_save_background_failed"));
return;
}
// 如果本 bg 还未在列表中,则添加
if (!this.settings.backgrounds.find((item) => item.id === bg.id)) {
this.settings.backgrounds.push(bg);
// 这里需要刷新设置页面中的背景列表和时间规则
this.refreshActiveBackgrounds();
this.refreshActiveTimeRules();
}
// 由于保存远程图片时会将本地图片路径替换为远程路径,因此需要更新设置
await this.saveSettings();
new Notice(t("notice_save_background_success", { folderPath: this.settings.localBackgroundFolder }));
}
async saveRemoteImage(bg: BackgroundItem, folderPath: string): Promise<boolean> {
if (!folderPath) {
new Notice(t("notice_save_background_valid_folder_path_required"));
return false;
}
// 默认图片名为 bg.name + .jpg , 并规范化路径 移除禁止的字符: \ / : * ? " < > |
const imageName = bg.name.replace(/[\\/:*?"<>|]/g, "_") + ".jpg";
const localPath = `${folderPath}/${imageName}`;
// 判断路径是否存在,如果存在,由用户确定是否覆盖
const file = this.app.vault.getFileByPath(localPath);
if (file) {
const overwrite = await confirm(
this.app,
t("notice_save_background_overwrite_existing_file", { filePath: localPath })
);
if (!overwrite) return true; // 用户取消覆盖
}
// 这里添加保存远程图片的逻辑
const response = await requestUrl({ url: bg.value });
if (response.status < 200 || response.status >= 300) {
new Notice(t("notice_save_background_failed"), response.status);
return false;
}
const arrayBuffer = response.arrayBuffer;
// 如果需要覆盖,则先删除旧文件
if (file) {
await this.app.fileManager.trashFile(file);
}
await this.app.vault.createBinary(localPath, arrayBuffer);
// 默认将bg中的url替换为本地路径并将remoteUrl设置为原始url以作备份
bg.remoteUrl = bg.value;
bg.value = localPath;
// 这里提示用户转换了 value, 时间长点
new Notice(t("notice_save_background_converted", { oldPath: bg.remoteUrl, newPath: bg.value }), 5000);
return true;
}
// 从壁纸API获取随机图片URL
async fetchRandomWallpaperFromAPI(): Promise<BackgroundItem | null> {
if (!this.settings.enableRandomWallpaper) {
return null;
}
// 获取所有启用的API实例
const enabledApis = apiManager.getEnabledApis();
// 如果没有启用的API返回null
if (enabledApis.length === 0) {
console.warn("DTB: No enabled APIs found");
return null;
}
// 从启用的API中随机选择一个
const randomIndex = Math.floor(Math.random() * enabledApis.length);
const selectedApi = enabledApis[randomIndex];
try {
// 显示加载提示
const loadingNotice = new Notice(t("notice_api_fetching", { apiName: selectedApi.getName() }), 0);
// 使用选中的API获取壁纸
const wallpaperImages = await apiManager.getRandomWallpapers(selectedApi.getId());
// 关闭加载提示
loadingNotice.hide();
if (!wallpaperImages || wallpaperImages.length === 0) {
console.warn(`DTB: No images returned from API: ${selectedApi.getName()}`);
return null;
}
const randomImage = wallpaperImages[Math.floor(Math.random() * wallpaperImages.length)];
if (randomImage && randomImage.url) {
new Notice(t("notice_api_success_applied", { apiName: selectedApi.getName() }));
return {
id: selectedApi.generateBackgroundId(),
name: selectedApi.generateBackgroundName(),
type: "image",
value: randomImage.url,
};
} else {
new Notice(t("notice_api_failed_fetch", { apiName: selectedApi.getName() }));
console.warn(`DTB: No wallpaper image returned from API: ${selectedApi.getName()}`);
return null;
}
} catch (error) {
new Notice(t("notice_api_error_fetch", { apiName: selectedApi.getName(), error: error.message }));
console.error("DTB: Error fetching random wallpaper:", error);
return null;
}
}
// 将图片路径转换为可用的 CSS URL
getBgURL(bg: BackgroundItem): string {
const imagePath = bg.value;
// 判断是否是远程图片
if (this.isRemoteImage(imagePath)) {
return `url("${imagePath}")`;
}
// 本地图片路径(只接受 Vault 内的图片)
const file = this.app.vault.getFileByPath(imagePath);
if (file) {
const p = this.app.vault.getResourcePath(file);
if (p) {
return `url("${p}")`;
} else {
console.warn(`DTB: Resource path for ${imagePath} is empty`);
}
} else {
console.warn(`DTB: Image ${imagePath} not found or inaccessible`);
}
// 如果 value 表示的本地路径无效,则查看 bg 有没有 remoteUrl 备份链接
if (bg.remoteUrl) {
// 这里恢复备份, 按理在这做不太合适
bg.value = bg.remoteUrl;
void this.saveSettings(); // 保存设置
return `url("${bg.remoteUrl}")`;
}
// 否则
return "none";
}
isRemoteImage(imagePath: string): boolean {
return imagePath.startsWith("http://") || imagePath.startsWith("https://");
}
/**
*
*
*/
getCurrentTimeRule(): TimeRule | null {
if (this.settings.mode !== "time-based") return null;
const now = new Date();
const currentTimeMinutes = now.getHours() * 60 + now.getMinutes();
// 按 startTime 排序,优先匹配靠前的规则
const sortedRules = this.settings.timeRules
.filter((rule) => rule.enabled)
.map((rule) => {
const { startTime, endTime } = this.parseTimeRule(rule);
return { rule, startTime, endTime };
})
.sort((a, b) => a.startTime - b.startTime);
// 遍历排序后的规则,找到第一个匹配的
for (const { rule, startTime, endTime } of sortedRules) {
// 跨天时段处理:如 20:00-06:00
if (startTime > endTime) {
if (currentTimeMinutes >= startTime || currentTimeMinutes < endTime) {
return rule;
}
} else {
if (currentTimeMinutes >= startTime && currentTimeMinutes < endTime) {
return rule;
}
}
}
return null;
}
/**
*
*/
getNextRuleChangeTime(): number | null {
if (this.settings.mode !== "time-based" || this.settings.timeRules.length === 0) {
return null;
}
const now = new Date();
const currentTimeMinutes = now.getHours() * 60 + now.getMinutes();
// 收集所有启用的时段规则的开始和结束时间点
const timePoints: Array<{ time: number; isStart: boolean; rule: TimeRule }> = [];
for (const rule of this.settings.timeRules) {
if (!rule.enabled) continue;
const { startTime, endTime } = this.parseTimeRule(rule);
timePoints.push({ time: startTime, isStart: true, rule });
timePoints.push({ time: endTime, isStart: false, rule });
}
// 按时间排序
timePoints.sort((a, b) => a.time - b.time);
// 查找下一个时间点
let nextPoint = null;
// 首先查找今天剩余时间内的下一个时间点
for (const point of timePoints) {
if (point.time > currentTimeMinutes) {
nextPoint = point;
break;
}
}
// 如果今天没有找到,取明天的第一个时间点
if (!nextPoint && timePoints.length > 0) {
nextPoint = timePoints[0];
}
if (!nextPoint) {
return null;
}
// 计算具体的时间戳
const targetDate = new Date(now);
const targetHours = Math.floor(nextPoint.time / 60);
const targetMinutes = nextPoint.time % 60;
targetDate.setHours(targetHours, targetMinutes, 0, 0);
// 如果目标时间已经过了,说明是明天的时间点
if (targetDate.getTime() <= now.getTime()) {
targetDate.setDate(targetDate.getDate() + 1);
}
return targetDate.getTime();
}
/**
*
*/
private parseTimeRule(rule: TimeRule) {
const [startHour, startMin] = rule.startTime.split(":").map(Number);
const [endHour, endMin] = rule.endTime.split(":").map(Number);
const startTime = startHour * 60 + startMin;
const endTime = endHour * 60 + endMin;
return { startTime, endTime };
}
/**
*
*/
refreshActiveSettings() {
this.settingTabs.forEach((tab) => {
if (tab.isActive()) {
tab.display();
}
});
this.events.emit("settings:changed", { key: "enabled", value: this.settings.enabled });
}
/**
*
*/
refreshActiveTimeRules() {
this.settingTabs.forEach((tab) => {
if (tab.isActive()) {
tab.displayTimeRules();
}
});
this.events.emit("time-rules:changed", {});
}
/**
*
*/
refreshActiveBackgrounds() {
this.settingTabs.forEach((tab) => {
if (tab.isActive()) {
tab.displayBackgrounds();
}
});
this.events.emit("backgrounds:changed", {});
}
/**
* api列表
*/
refreshActiveApiList() {
this.settingTabs.forEach((tab) => {
if (tab.isActive()) {
tab.displayWallpaperApis();
}
});
this.events.emit("apis:changed", {});
}
}

View file

@ -0,0 +1,382 @@
/**
* API
* DTBSettingTab section API
*/
import { Notice, Setting } from "obsidian";
import { logger } from "../../core/logger";
import { t } from "../../i18n";
import { WallpaperApiEditorModal } from "../../modals";
import type DynamicThemeBackgroundPlugin from "../../plugin";
import type { BackgroundItem, DTBSettings } from "../../types";
import { DragSort, generateId } from "../../utils";
import {
ApiStateSubscriber,
BaseWallpaperApi,
WallpaperApiConfig,
WallpaperApiType,
apiManager,
} from "../../wallpaper-apis";
export class ApiSettingsSection {
private plugin: DynamicThemeBackgroundPlugin;
private defaultSettings: DTBSettings;
private componentId: string;
private container!: HTMLElement;
private apiListContainer!: HTMLElement;
private apiDragSort?: DragSort<WallpaperApiConfig>;
/** 外部回调:刷新背景列表 */
private onBackgroundsChanged?: () => void;
/** 外部回调:刷新时间规则 */
private onTimeRulesChanged?: () => void;
constructor(
plugin: DynamicThemeBackgroundPlugin,
defaultSettings: DTBSettings,
callbacks?: {
onBackgroundsChanged?: () => void;
onTimeRulesChanged?: () => void;
}
) {
this.plugin = plugin;
this.defaultSettings = defaultSettings;
this.componentId = generateId("api-settings-section");
this.onBackgroundsChanged = callbacks?.onBackgroundsChanged;
this.onTimeRulesChanged = callbacks?.onTimeRulesChanged;
}
/*
* API
*/
display(container: HTMLElement): void {
this.container = container;
const containerEl = this.container;
containerEl.empty();
// 壁纸API管理标题
new Setting(containerEl).setName(t("wallpaper_api_management_title")).setHeading();
// 添加 API 按钮
const buttonContainer = containerEl.createDiv("dtb-large-button-container");
new Setting(buttonContainer)
.setName(t("add_api_name"))
.setDesc(t("add_api_desc"))
.addButton((button) => {
button.setButtonText(t("add_api_button"));
button.onClick(() => this.showAddWallpaperApiModal());
})
// 添加新增所有默认 API 设置的恢复按钮,如果 API 已经存在则不添加
.addExtraButton((button) => {
button.setIcon("refresh-cw");
button.setTooltip(t("restore_default_apis_tooltip"));
button.onClick(() => {
// 重新生成默认设置以获取最新的默认 API
const defaultApis = this.defaultSettings.wallpaperApis;
// 遍历默认 API检查是否已存在
for (const apiConfig of defaultApis) {
const existingApi = this.plugin.settings.wallpaperApis.find(
(api) => api.id === apiConfig.id
);
if (!existingApi) {
// 如果不存在,则添加并创建 API 实例
this.plugin.settings.wallpaperApis.push(apiConfig);
void apiManager.createApi(apiConfig);
}
}
new Notice(t("restore_default_apis_success"));
void this.plugin.saveSettings();
this.display(this.container);
});
});
// 添加 API 提示
const hint = containerEl.createDiv("dtb-hint");
hint.textContent = t("wallpaper_api_hint");
// 显示现有API列表
this.apiListContainer = containerEl.createDiv("dtb-section-container");
this.displayWallpaperApis();
}
/*
* API
*/
displayWallpaperApis(): void {
const container = this.apiListContainer;
this.apiDragSort?.disableAllDrag();
container.empty();
// 初始化 API 拖拽排序
this.apiDragSort = new DragSort<WallpaperApiConfig>({
container,
items: this.plugin.settings.wallpaperApis,
getItemId: (api) => api.id,
itemClass: "dtb-draggable",
idDataAttribute: "apiId",
onReorder: async (reorderedApis) => {
this.plugin.settings.wallpaperApis = reorderedApis;
await this.plugin.saveSettings();
this.displayWallpaperApis();
},
});
// API 列表
this.plugin.settings.wallpaperApis.forEach((apiConfig: WallpaperApiConfig, index: number) => {
const apiInstance = apiManager.getApiById(apiConfig.id);
if (!apiInstance) {
logger.warn(`API instance not found for ${apiConfig.name}`);
return;
}
const setting = new Setting(container).setName(apiConfig.name).setDesc(apiInstance.getDescription());
// 在设置项的控件区域直接添加类型标签
setting.controlEl.createSpan({ text: apiConfig.type ?? "Unknown", cls: "dtb-badge" });
// 注意:状态指示器和启用按钮的状态都以 API 实例的状态为准,配置项的 enabled 字段仅用于初始状态和保存设置时的同步。
// 添加状态指示器
const statusIndicator = setting.controlEl.createDiv("dtb-api-status");
const statusDot = statusIndicator.createDiv("dtb-api-status-dot");
const statusText = statusIndicator.createSpan();
// 根据API的启用状态设置初始状态
if (apiInstance.getEnabled()) {
statusDot.addClass("enabled");
statusText.textContent = t("status_enabled");
} else {
statusDot.addClass("disabled");
statusText.textContent = t("status_disabled");
}
// 创建 toggle 并保存引用
let toggleComponent: { setValue: (value: boolean) => void; getValue: () => boolean } | null = null;
setting.addToggle((toggle) => {
toggleComponent = toggle; // 保存 toggle 引用
const toggleEl = toggle.setValue(apiInstance.getEnabled());
// 使用智能API管理方法
toggleEl.onChange(async (value) => {
// 禁用toggle防止用户重复点击并添加loading样式
toggle.setDisabled(true);
toggleEl.toggleEl.addClass("dtb-loading");
try {
let success: boolean;
if (value) {
// 使用智能启用方法
success = await apiManager.enableApi(apiConfig.id);
} else {
// 使用智能禁用方法
success = await apiManager.disableApi(apiConfig.id);
}
const action = value ? t("action_enable") : t("action_disable");
if (!success) {
new Notice(
t("notice_api_failed_enable_disable", { action, apiName: apiConfig.name }),
3000
);
} else {
new Notice(
t("notice_api_success_enable_disable", { action, apiName: apiConfig.name }),
3000
);
}
} catch (error) {
logger.error(`Error ${value ? "enabling" : "disabling"} API:`, error);
const action = value ? t("action_enable") : t("action_disable");
new Notice(
t("notice_api_error_enable_disable", { action, apiName: apiConfig.name }),
3000
);
} finally {
// 重新启用toggle并移除loading样式
toggle.setDisabled(false);
toggleEl.toggleEl.removeClass("dtb-loading");
}
});
return toggleEl;
});
// 订阅状态变化,使用 ApiStateSubscriber 对象进行标识
const subscriber = new ApiStateSubscriber("toggle", this.componentId, apiConfig.id);
apiManager.stateManager.subscribe(subscriber, async (state) => {
// 更新状态点的样式
statusDot.removeClass("enabled", "disabled", "error", "loading");
if (state.isLoading) {
statusDot.addClass("loading");
statusText.textContent = t("status_loading");
} else if (state.error) {
statusDot.addClass("error");
statusText.textContent = t("status_error");
statusText.title = state.error;
// 同步更新 toggle 状态
if (toggleComponent && toggleComponent.getValue() !== false) {
toggleComponent.setValue(false);
}
apiConfig.enabled = false;
await this.plugin.saveSettings();
} else if (state.instanceEnabled) {
statusDot.addClass("enabled");
statusText.textContent = t("status_enabled");
// 同步更新 toggle 状态
if (toggleComponent && toggleComponent.getValue() !== true) {
toggleComponent.setValue(true);
}
apiConfig.enabled = true;
await this.plugin.saveSettings();
} else {
statusDot.addClass("disabled");
statusText.textContent = t("status_disabled");
// 同步更新 toggle 状态
if (toggleComponent && toggleComponent.getValue() !== false) {
toggleComponent.setValue(false);
}
apiConfig.enabled = false;
await this.plugin.saveSettings();
}
});
setting
.addButton((button) =>
button
.setButtonText(t("button_add"))
.setTooltip(t("add_api_bg_tooltip"))
.onClick(async () => {
await this.fetchWallpaperFromApi(apiInstance);
})
)
.addButton((button) =>
button.setButtonText(t("button_edit")).onClick(() => {
this.showEditWallpaperApiModal(apiConfig, index);
})
)
.addButton((button) =>
button.setButtonText(t("button_delete")).onClick(() => {
// 删除API实例
apiManager.deleteApi(apiConfig.id);
// 删除插件设置中的API配置
this.plugin.settings.wallpaperApis = this.plugin.settings.wallpaperApis.filter(
(api) => api.id !== apiConfig.id
);
void this.plugin.saveSettings();
this.displayWallpaperApis();
})
);
// 设置拖拽属性
setting.settingEl.addClass("dtb-draggable");
setting.settingEl.dataset.apiId = apiConfig.id;
// 添加通用条目样式类
setting.settingEl.addClass("dtb-button-container"); // 按钮样式
// 启用拖拽功能
this.apiDragSort?.enableDragForElement(setting.settingEl, apiConfig);
});
}
// 显示添加壁纸API的模态窗口
private showAddWallpaperApiModal() {
const emptyConfig: WallpaperApiConfig = {
id: "",
name: "",
type: WallpaperApiType.Custom,
baseUrl: "",
enabled: false,
params: {},
};
const modal = new WallpaperApiEditorModal(this.plugin.app, emptyConfig, (apiConfig) => {
// 创建新的API实例
void apiManager.createApi(apiConfig);
// 添加到插件设置中
this.plugin.settings.wallpaperApis.push(apiConfig);
void this.plugin.saveSettings();
// 这里仅需刷新 api 列表
this.displayWallpaperApis();
});
modal.open();
}
// 显示编辑壁纸API的模态窗口
private showEditWallpaperApiModal(apiConfig: WallpaperApiConfig, index: number) {
const modal = new WallpaperApiEditorModal(this.plugin.app, apiConfig, (updatedConfig) => {
// 有可能api类型也修改了干脆重新创建API实例覆盖原来的
void apiManager.createApi(updatedConfig);
this.plugin.settings.wallpaperApis[index] = updatedConfig;
void this.plugin.saveSettings();
// 这里仅需刷新 api 列表
this.displayWallpaperApis();
});
modal.open();
}
// 从API获取壁纸并添加到背景列表
private async fetchWallpaperFromApi(api: BaseWallpaperApi) {
if (!api.getEnabled()) {
new Notice(t("notice_api_disabled", { apiName: api.getName() }));
return;
}
try {
// 显示加载提示
const loadingNotice = new Notice(t("notice_api_fetching", { apiName: api.getName() }), 0);
// 使用API管理器获取随机壁纸
const wallpaperImages = await apiManager.getRandomWallpapers(api.getId());
// 关闭加载提示
loadingNotice.hide();
if (wallpaperImages) {
// 创建新的图片背景项
const newBg: BackgroundItem = {
id: api.generateBackgroundId(),
name: api.generateBackgroundName(),
type: "image",
value: wallpaperImages[0].url,
};
// 添加到背景列表
this.plugin.settings.backgrounds.push(newBg);
await this.plugin.saveSettings();
// 立即应用这个背景
this.plugin.background = newBg;
this.plugin.updateStyleCss();
// 这里仅需刷新背景列表和时间规则
this.onBackgroundsChanged?.();
this.onTimeRulesChanged?.();
new Notice(t("notice_api_success_applied", { apiName: api.getName() }));
} else {
new Notice(t("notice_api_failed_fetch", { apiName: api.getName() }));
}
} catch (error) {
new Notice(t("notice_api_error_fetch", { apiName: api.getName(), error: (error as Error).message }));
logger.error("Error fetching wallpaper:", error);
}
}
/**
*
*/
cleanup(): void {
// 清理拖拽排序实例
this.apiDragSort?.disableAllDrag();
// 使用组件ID清理该组件的所有订阅
apiManager.stateManager.cleanupByComponent(this.componentId);
}
}

View file

@ -0,0 +1,283 @@
import { Setting } from "obsidian";
import { t } from "../../i18n";
import type DynamicThemeBackgroundPlugin from "../../plugin";
import type { DTBSettings } from "../../types";
import { addDropdownOptionHoverTooltip } from "../../utils";
export class BasicSettingsSection {
private plugin: DynamicThemeBackgroundPlugin;
private defaultSettings: DTBSettings;
private container!: HTMLElement;
constructor(plugin: DynamicThemeBackgroundPlugin, defaultSettings: DTBSettings) {
this.plugin = plugin;
this.defaultSettings = defaultSettings;
}
display(container: HTMLElement): void {
this.container = container;
const containerEl = this.container;
containerEl.empty();
// 基础设置标题
new Setting(containerEl).setName(t("basic_settings_title")).setHeading();
// 是否启用插件
new Setting(containerEl)
.setName(t("enable_plugin_name"))
.setDesc(t("enable_plugin_desc"))
.addToggle((toggle) =>
toggle.setValue(this.plugin.settings.enabled).onChange(async (value) => {
this.plugin.settings.enabled = value;
await this.plugin.saveSettings();
if (value) {
this.plugin.startBackgroundManager();
} else {
this.plugin.stopBackgroundManager();
}
})
)
.addExtraButton((button) =>
button
.setIcon("refresh-cw")
.setTooltip(t("reload_plugin_tooltip"))
.onClick(() => {
// 重新启动背景管理器
if (this.plugin.settings.enabled) {
this.plugin.startBackgroundManager();
}
// 强制更新当前背景
void this.plugin.updateBackground(true);
})
);
// 是否开启状态栏
new Setting(containerEl)
.setName(t("enable_status_bar_name"))
.setDesc(t("enable_status_bar_desc") + t("status_bar_title").replace(/\n/g, " "))
.addToggle((toggle) =>
toggle.setValue(this.plugin.settings.statusBarEnabled).onChange(async (value) => {
this.plugin.settings.statusBarEnabled = value;
await this.plugin.saveSettings();
if (value) {
this.plugin.activateStatusBar();
} else {
this.plugin.deactivateStatusBar();
}
})
);
// 外观设置
new Setting(containerEl).setName(t("appearance_settings_title")).setHeading();
// 外观设置提示(全局外观优先级说明)
const appearanceHint = containerEl.createDiv("dtb-hint");
appearanceHint.textContent = t("appearance_settings_hint");
// 背景模糊度设置
new Setting(containerEl)
.setName(t("blur_depth_name"))
.setDesc(t("blur_depth_desc"))
.addSlider((slider) =>
slider
.setLimits(0, 30, 1)
.setValue(this.plugin.settings.blurDepth)
.setDynamicTooltip()
.onChange(async (value: number) => {
this.plugin.settings.blurDepth = value;
await this.plugin.saveSettings();
this.plugin.updateStyleCss();
})
)
.addExtraButton((button) =>
button
.setIcon("reset")
.setTooltip(t("reset_blur_tooltip"))
.onClick(async () => {
this.plugin.settings.blurDepth = this.defaultSettings.blurDepth;
await this.plugin.saveSettings();
this.plugin.updateStyleCss();
this.display(this.container);
})
);
// 背景亮度设置
new Setting(containerEl)
.setName(t("brightness_name"))
.setDesc(t("brightness_desc"))
.addSlider((slider) =>
slider
.setLimits(0, 1.5, 0.01)
.setValue(this.plugin.settings.brightness4Bg)
.setDynamicTooltip()
.onChange(async (value: number) => {
this.plugin.settings.brightness4Bg = value;
await this.plugin.saveSettings();
this.plugin.updateStyleCss();
})
)
.addExtraButton((button) =>
button
.setIcon("reset")
.setTooltip(t("reset_brightness_tooltip"))
.onClick(async () => {
this.plugin.settings.brightness4Bg = this.defaultSettings.brightness4Bg;
await this.plugin.saveSettings();
this.plugin.updateStyleCss();
this.display(this.container);
})
);
// 背景饱和度设置
new Setting(containerEl)
.setName(t("saturate_name"))
.setDesc(t("saturate_desc"))
.addSlider((slider) =>
slider
.setLimits(0, 2, 0.01)
.setValue(this.plugin.settings.saturate4Bg)
.setDynamicTooltip()
.onChange(async (value: number) => {
this.plugin.settings.saturate4Bg = value;
await this.plugin.saveSettings();
this.plugin.updateStyleCss();
})
)
.addExtraButton((button) =>
button
.setIcon("reset")
.setTooltip(t("reset_saturate_tooltip"))
.onClick(async () => {
this.plugin.settings.saturate4Bg = this.defaultSettings.saturate4Bg;
await this.plugin.saveSettings();
this.plugin.updateStyleCss();
this.display(this.container);
})
);
// 背景颜色和透明度设置(暗/亮两套,同一行:暗在前,亮在后)
{
const setting = new Setting(containerEl).setName(t("bg_mask_color_name")).setDesc(t("bg_mask_color_desc"));
// 暗主题标签
const darkLabel = setting.controlEl.createSpan({ text: "🌙" });
darkLabel.setAttribute("title", t("overlay_dark_tooltip"));
// 暗主题:颜色
setting.addColorPicker((colorPicker) =>
colorPicker
.setValue(this.plugin.settings.bgColorDark ?? this.defaultSettings.bgColorDark)
.onChange(async (value: string) => {
this.plugin.settings.bgColorDark = value;
await this.plugin.saveSettings();
this.plugin.updateStyleCss();
})
);
// 暗主题:透明度
setting.addSlider((slider) =>
slider
.setLimits(0, 1, 0.01)
.setValue(this.plugin.settings.bgColorOpacityDark ?? this.defaultSettings.bgColorOpacityDark)
.setDynamicTooltip()
.onChange(async (value: number) => {
this.plugin.settings.bgColorOpacityDark = value;
await this.plugin.saveSettings();
this.plugin.updateStyleCss();
})
);
// 亮主题标签
const lightLabel = setting.controlEl.createSpan({ text: "☀️" });
lightLabel.setAttribute("title", t("overlay_light_tooltip"));
// 亮主题:颜色
setting.addColorPicker((colorPicker) =>
colorPicker
.setValue(this.plugin.settings.bgColorLight ?? this.defaultSettings.bgColorLight)
.onChange(async (value: string) => {
this.plugin.settings.bgColorLight = value;
await this.plugin.saveSettings();
this.plugin.updateStyleCss();
})
);
// 亮主题:透明度
setting.addSlider((slider) =>
slider
.setLimits(0, 1, 0.01)
.setValue(this.plugin.settings.bgColorOpacityLight ?? this.defaultSettings.bgColorOpacityLight)
.setDynamicTooltip()
.onChange(async (value: number) => {
this.plugin.settings.bgColorOpacityLight = value;
await this.plugin.saveSettings();
this.plugin.updateStyleCss();
})
);
// 重置(两套新字段)
setting.addExtraButton((button) =>
button
.setIcon("reset")
.setTooltip(t("reset_bg_mask_color_tooltip"))
.onClick(async () => {
this.plugin.settings.bgColorDark = this.defaultSettings.bgColorDark;
this.plugin.settings.bgColorOpacityDark = this.defaultSettings.bgColorOpacityDark;
this.plugin.settings.bgColorLight = this.defaultSettings.bgColorLight;
this.plugin.settings.bgColorOpacityLight = this.defaultSettings.bgColorOpacityLight;
await this.plugin.saveSettings();
this.plugin.updateStyleCss();
this.display(this.container);
})
);
}
// 背景填充方式设置
new Setting(containerEl)
.setName(t("bg_size_name"))
.setDesc(t("bg_size_desc"))
.addDropdown((dropdown) => {
// 添加下拉选项
dropdown.addOption("intelligent", "Intelligent");
dropdown.addOption("cover", "Cover");
dropdown.addOption("contain", "Contain");
dropdown.addOption("auto", "Auto");
// 使用专门的悬停选项方法添加 tooltip推荐用法
addDropdownOptionHoverTooltip(
dropdown,
{
cover: t("bg_size_option_cover"),
contain: t("bg_size_option_contain"),
auto: t("bg_size_option_auto"),
intelligent: t("bg_size_option_intelligent"),
},
{
defaultTooltip: t("bg_size_desc"),
updateOnChange: true, // 选择后也更新整个下拉框的 tooltip
}
);
dropdown
.setValue(this.plugin.settings.bgSize)
.onChange(async (value: string) => {
const bgSize = value as DTBSettings["bgSize"];
this.plugin.settings.bgSize = bgSize;
await this.plugin.saveSettings();
this.plugin.updateStyleCss();
});
return dropdown;
})
.addExtraButton((button) =>
button
.setIcon("reset")
.setTooltip(t("reset_bg_size_tooltip"))
.onClick(async () => {
this.plugin.settings.bgSize = this.defaultSettings.bgSize;
await this.plugin.saveSettings();
this.plugin.updateStyleCss();
this.display(this.container);
})
);
}
cleanup(): void {
// Basic settings has no subscriptions to clean up
}
}

View file

@ -0,0 +1,448 @@
/**
*
* DTBSettingTab
*/
import { Notice, Setting, TFile } from "obsidian";
import { logger } from "../../core/logger";
import { t } from "../../i18n";
import { BackgroundModal, ImageFolderSuggestModal } from "../../modals";
import type DynamicThemeBackgroundPlugin from "../../plugin";
import type { BackgroundItem, DTBSettings } from "../../types";
import { DragSort, generateId } from "../../utils";
export class BgManagementSection {
private plugin: DynamicThemeBackgroundPlugin;
private defaultSettings: DTBSettings;
private container!: HTMLElement;
private bgListContainer!: HTMLElement;
private backgroundDragSort?: DragSort<BackgroundItem>;
/** Callback invoked when backgrounds change and external sections (e.g. time rules) need refresh */
private onChanged?: () => void;
constructor(
plugin: DynamicThemeBackgroundPlugin,
defaultSettings: DTBSettings,
options?: { onChanged?: () => void }
) {
this.plugin = plugin;
this.defaultSettings = defaultSettings;
this.onChanged = options?.onChanged;
}
display(container: HTMLElement): void {
this.container = container;
const containerEl = this.container;
containerEl.empty();
// 背景管理标题
new Setting(containerEl).setName(t("bg_management_title")).setHeading();
// 保存远程图片的本地路径
const imageFolderInputContainer = containerEl.createDiv("setting-item dtb-flex-container-spaced");
imageFolderInputContainer.createEl("label", { text: t("save_image_path_title") });
const valueInput = imageFolderInputContainer.createEl("input", {
type: "text",
title: t("save_image_path_title"),
placeholder: t("save_image_path_placeholder"),
value: this.plugin.settings.localBackgroundFolder ?? "",
cls: "dtb-flex-1",
});
valueInput.oninput = () => {
this.plugin.settings.localBackgroundFolder = valueInput.value;
void this.plugin.saveSettings();
};
const browseButton = imageFolderInputContainer.createEl("button", {
type: "button",
text: t("button_browse"),
cls: "dtb-button",
});
browseButton.onclick = () => {
const modal = new ImageFolderSuggestModal(this.plugin.app, (imagePath: string) => {
valueInput.value = imagePath;
this.plugin.settings.localBackgroundFolder = imagePath;
void this.plugin.saveSettings();
});
modal.open();
};
// 添加背景的一组按钮
const buttonContainer = containerEl.createDiv("dtb-large-button-container");
new Setting(buttonContainer)
.setName(t("add_new_bg_name"))
.setDesc(t("add_new_bg_desc"))
.addButton((button) =>
button.setButtonText(t("add_image_bg_button")).onClick(() => this.showAddBackgroundModal("image"))
)
.addButton((button) =>
button.setButtonText(t("add_color_bg_button")).onClick(() => this.showAddBackgroundModal("color"))
)
.addButton((button) =>
button.setButtonText(t("add_gradient_bg_button")).onClick(() => this.showAddBackgroundModal("gradient"))
)
.addButton((button) =>
button.setButtonText(t("add_folder_bg_button")).onClick(() => this.showAddFolderModal())
)
.addExtraButton((button) =>
button
.setIcon("reset")
.setTooltip(t("restore_default_bg_tooltip"))
.onClick(() => this.restoreDefaultBackgrounds())
);
// 添加拖拽提示
const dragHint = containerEl.createDiv("dtb-hint");
dragHint.textContent = t("background_management_hint");
this.bgListContainer = containerEl.createDiv("dtb-section-container");
this.displayBackgrounds();
}
// 在指定的容器元素中渲染所有背景项
displayBackgrounds(): void {
const container = this.bgListContainer;
this.backgroundDragSort?.disableAllDrag();
container.empty();
// 初始化背景拖拽排序
this.backgroundDragSort = new DragSort<BackgroundItem>({
container,
items: this.plugin.settings.backgrounds,
getItemId: (bg) => bg.id,
itemClass: "dtb-draggable",
idDataAttribute: "bgId",
onReorder: async (reorderedBackgrounds) => {
this.plugin.settings.backgrounds = reorderedBackgrounds;
await this.plugin.saveSettings();
// 这里仅需刷新背景列表和时间规则列表
this.displayBackgrounds();
this.onChanged?.();
},
});
this.plugin.settings.backgrounds.forEach((bg: BackgroundItem, index: number) => {
const bgEl = container.createDiv("dtb-item dtb-draggable");
// 添加拖拽相关属性
bgEl.draggable = true;
bgEl.dataset.bgId = bg.id;
bgEl.dataset.index = index.toString();
// 添加拖拽手柄
const dragHandle = bgEl.createDiv("dtb-drag-handle");
dragHandle.textContent = "⋮⋮"; // 使用双点符号作为拖拽手柄
dragHandle.title = t("drag_handle_tooltip");
// 背景名称
const contentDiv = bgEl.createDiv("dtb-bg-content");
contentDiv.createSpan({ text: bg.name, cls: "dtb-bg-name" });
// 如果是启用背景,添加图标
if (bg.id === this.plugin.background?.id) {
const icon = contentDiv.createSpan();
icon.setText("🔥");
icon.title = t("current_background");
}
// 背景类型
contentDiv.createSpan({ text: bg.type, cls: "dtb-badge" });
// 预览图
const preview = contentDiv.createDiv("dtb-bg-preview");
this.setPreviewBackground(preview, bg);
// 操作按钮
const actions = contentDiv.createDiv("dtb-button-container");
// 预览按钮
actions.createEl("button", { text: t("button_preview") }).onclick = () => {
this.plugin.background = bg;
this.plugin.settings.currentIndex = index; // 更新当前索引
void this.plugin.saveSettings();
this.plugin.updateStyleCss();
this.displayBackgrounds(); // 刷新激活图标
};
// 保存按钮
actions.createEl("button", { text: t("button_save") }).onclick = () => {
void this.plugin.saveBackground(bg);
};
// 编辑按钮
actions.createEl("button", { text: t("button_edit") }).onclick = () => {
this.showEditBackgroundModal(bg, index);
};
// 删除按钮
actions.createEl("button", { text: t("button_delete") }).onclick = () => {
// 使用 filter 方法删除
this.plugin.settings.backgrounds = this.plugin.settings.backgrounds.filter(
(b: BackgroundItem) => b.id !== bg.id
);
void this.plugin.saveSettings();
// 这里仅需刷新背景列表和时间规则列表
this.displayBackgrounds();
this.onChanged?.();
};
// 启用拖拽功能
this.backgroundDragSort?.enableDragForElement(bgEl, bg);
});
}
cleanup(): void {
this.backgroundDragSort?.disableAllDrag();
}
// 显示添加或编辑背景的模态窗口
private showAddBackgroundModal(type: "image" | "color" | "gradient"): void {
// 创建一个新的背景项,初始值为空
const bg: BackgroundItem = {
id: "",
name: "",
type,
value: "",
};
const modal = new BackgroundModal(this.plugin.app, this.plugin, bg, (newBg: BackgroundItem) => {
if (!newBg.name.trim() || !newBg.value.trim()) {
new Notice(t("notice_name_and_value_required"));
return;
}
newBg.name = newBg.name.trim();
newBg.value = newBg.value.trim();
// 生成唯一ID
newBg.id = generateId("bg");
// 添加到设置中
this.plugin.settings.backgrounds.push(newBg);
void this.plugin.saveSettings();
// 这里仅需刷新背景列表和时间规则列表
this.displayBackgrounds();
this.onChanged?.();
});
modal.open();
}
// 显示编辑背景的模态窗口
private showEditBackgroundModal(bg: BackgroundItem, index: number): void {
const modal = new BackgroundModal(this.plugin.app, this.plugin, bg, (newBg: BackgroundItem) => {
if (!newBg.name.trim() || !newBg.value.trim()) {
new Notice(t("notice_name_and_value_required"));
return;
}
newBg.name = newBg.name.trim();
newBg.value = newBg.value.trim();
// 更新现有背景项
this.plugin.settings.backgrounds[index] = newBg;
void this.plugin.saveSettings();
// 如果当前正在使用这个背景,则更新显示
if (this.plugin.background?.id === bg.id) {
this.plugin.background = this.plugin.settings.backgrounds[index];
this.plugin.updateStyleCss();
}
// 这里仅需刷新背景列表和时间规则列表
this.displayBackgrounds();
this.onChanged?.();
});
// 打开模态窗口(初始值已在 BackgroundModal.onOpen() 中从 bgItem 设置)
modal.open();
}
private showAddFolderModal(): void {
const modal = new ImageFolderSuggestModal(this.plugin.app, (folderPath: string) => {
if (!folderPath.trim()) {
new Notice(t("notice_valid_folder_path_required"));
return;
}
// 检查文件夹是否存在
const folder = this.plugin.app.vault.getAbstractFileByPath(folderPath);
if (!folder) {
new Notice(t("notice_folder_not_found"));
return;
}
// 处理文件夹中的图片文件
this.addImagesFromFolder(folderPath)
.then(() => {
new Notice(t("notice_folder_added_successfully", { folderPath }));
})
.catch((error) => {
logger.error("Error adding images from folder:", error);
new Notice(t("notice_error_adding_folder_images"));
});
});
modal.open();
}
private async restoreDefaultBackgrounds() {
// 重新生成默认设置以获取最新的默认背景
const defaultBackgrounds = this.defaultSettings.backgrounds;
let addedCount = 0;
// 遍历默认背景,只添加不存在的
for (const defaultBg of defaultBackgrounds) {
const existingBg = this.plugin.settings.backgrounds.find((bg) => bg.id === defaultBg.id);
if (!existingBg) {
// 创建新的背景项,确保 ID 唯一
const newBg: BackgroundItem = {
id: defaultBg.id,
name: defaultBg.name,
type: defaultBg.type,
value: defaultBg.value,
};
this.plugin.settings.backgrounds.push(newBg);
addedCount++;
}
}
if (addedCount > 0) {
await this.plugin.saveSettings();
// 这里仅需刷新背景列表和时间规则列表
this.displayBackgrounds();
this.onChanged?.();
new Notice(
t("restore_default_bg_success", {
count: addedCount.toString(),
})
);
} else {
new Notice(t("restore_default_bg_no_new"));
}
}
// 设置预览元素的背景样式 * 使用 CSS 自定义属性而不是内联样式,遵循 Obsidian 官方建议
private setPreviewBackground(preview: HTMLElement, bg: BackgroundItem): void {
// 移除之前的类型特定类名
preview.removeClass("dtb-preview-image", "dtb-preview-color", "dtb-preview-gradient");
// 清除之前设置的 CSS 自定义属性
preview.setCssProps({
"--dtb-preview-bg-image": "",
"--dtb-preview-bg": "",
});
switch (bg.type) {
case "image": {
preview.addClass("dtb-preview-image");
const sanitizedImagePath = this.plugin.getBgURL(bg);
// 只有当图片路径有效时才设置 CSS 变量
if (sanitizedImagePath && sanitizedImagePath !== "none") {
preview.setCssProps({
"--dtb-preview-bg-image": sanitizedImagePath,
});
}
break;
}
case "color":
case "gradient": {
preview.addClass(`dtb-preview-${bg.type}`);
// 验证颜色/渐变值的有效性
if (bg.value && bg.value.trim()) {
preview.setCssProps({
"--dtb-preview-bg": bg.value,
});
}
break;
}
default:
logger.warn(`Unknown background type: ${bg.type as string}`);
break;
}
}
// 添加文件夹中的图片到背景列表
private async addImagesFromFolder(folderPath: string) {
try {
// 标准化路径:移除开头和结尾的斜杠,只处理 vault 内的相对路径
folderPath = folderPath.replace(/^\/+|\/+$/g, "");
let folderFiles: TFile[] = [];
if (folderPath !== "") {
// 尝试获取指定文件夹
const folder = this.plugin.app.vault.getFolderByPath(folderPath);
if (folder) {
// 只获取该文件夹下的直接子文件(不递归)
folderFiles = this.plugin.app.vault.getFiles().filter((file) => {
const fileDir = file.path.substring(0, file.path.lastIndexOf("/"));
return fileDir === folderPath;
});
} else {
new Notice(t("folder_not_found"));
return;
}
}
if (folderFiles.length === 0) {
new Notice(t("folder_not_found"));
return;
}
await this.processImageFiles(folderFiles, folderPath);
} catch (error) {
logger.error("Error scanning folder:", error);
new Notice(t("folder_scan_error", { error: (error as Error).message }));
}
}
// 处理文件夹中的图片文件
private async processImageFiles(files: TFile[], folderPath: string) {
// 支持的图片格式
const imageExtensions = [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".svg"];
// 过滤出图片文件
const imageFiles = files.filter((file) => imageExtensions.some((ext) => file.path.toLowerCase().endsWith(ext)));
if (imageFiles.length === 0) {
new Notice(t("folder_not_found"));
return;
}
let addedCount = 0;
for (const file of imageFiles) {
// 检查是否已存在相同路径的背景
const existingBg = this.plugin.settings.backgrounds.find(
(bg) => bg.type === "image" && bg.value === file.path
);
if (!existingBg) {
const fileName = file.name.replace(/\.[^/.]+$/, ""); // 移除扩展名
// 只保留最后一级文件夹名称,避免长路径影响观感
const folderName = folderPath === "" ? "root" : (folderPath.split("/").pop() ?? folderPath);
const newBg: BackgroundItem = {
id: generateId("bg"),
name: `${fileName} (${folderName})`,
type: "image",
value: file.path,
};
this.plugin.settings.backgrounds.push(newBg);
addedCount++;
}
}
if (addedCount > 0) {
await this.plugin.saveSettings();
// 这里仅需刷新背景列表和时间规则列表
this.displayBackgrounds();
this.onChanged?.();
new Notice(t("folder_scan_success", { count: addedCount.toString() }));
} else {
new Notice(t("folder_no_new_images"));
}
}
}

View file

@ -0,0 +1,4 @@
export { ApiSettingsSection } from "./api-settings";
export { BasicSettingsSection } from "./basic-settings";
export { BgManagementSection } from "./bg-management";
export { ModeSettingsSection } from "./mode-settings";

View file

@ -0,0 +1,285 @@
import { Notice, Setting } from "obsidian";
import { t } from "../../i18n";
import { ConfirmModal, TimeRuleModal } from "../../modals";
import type DynamicThemeBackgroundPlugin from "../../plugin";
import type { DTBSettings, TimeRule } from "../../types";
import { DragSort, addDropdownTooltip, addEnhancedDropdownTooltip, generateId } from "../../utils";
export class ModeSettingsSection {
private plugin: DynamicThemeBackgroundPlugin;
private defaultSettings: DTBSettings;
private container!: HTMLElement;
private timeRulesContainer: HTMLElement | null = null;
private timeRuleDragSort: DragSort<TimeRule> | null = null;
constructor(plugin: DynamicThemeBackgroundPlugin, defaultSettings: DTBSettings) {
this.plugin = plugin;
this.defaultSettings = defaultSettings;
}
display(container: HTMLElement): void {
this.container = container;
const containerEl = this.container;
containerEl.empty();
// 模式设置标题
new Setting(containerEl).setName(t("mode_settings_title")).setHeading();
new Setting(containerEl)
.setName(t("switch_mode_name"))
.setDesc(t("switch_mode_desc"))
.addDropdown((dropdown) => {
dropdown
.addOption("time-based", t("mode_time_based"))
.addOption("interval", t("mode_interval"))
.addOption("manual", t("mode_manual"))
.setValue(this.plugin.settings.mode)
.onChange(async (value: string) => {
const mode = value as DTBSettings["mode"];
this.plugin.settings.mode = mode;
await this.plugin.saveSettings();
this.plugin.startBackgroundManager();
this.display(this.container);
});
// 添加模式切换的 tooltip
addDropdownTooltip(
dropdown,
{
"time-based": t("mode_time_based_tooltip"),
interval: t("mode_interval_tooltip"),
manual: t("mode_manual_tooltip"),
},
t("switch_mode_desc")
);
return dropdown;
});
// 时间规则(仅在时间模式下显示)
if (this.plugin.settings.mode === "time-based") {
new Setting(containerEl).setName(t("time_rules_title")).setHeading();
const buttonContainer = containerEl.createDiv("dtb-large-button-container");
new Setting(buttonContainer)
.setName(t("manage_time_rules_name"))
.setDesc(t("manage_time_rules_desc"))
.addButton((button) => {
button.setButtonText(t("add_time_rule_button"));
button.setTooltip(t("add_time_rule_tooltip"));
button.onClick(() => this.showTimeRuleModal());
})
.addButton((button) => {
button.setButtonText(t("clear_time_rules_button"));
button.setTooltip(t("clear_time_rules_tooltip"));
button.onClick(() => {
new ConfirmModal(this.plugin.app, {
message: t("confirm_clear_time_rules"),
onConfirm: () => {
this.plugin.settings.timeRules = [];
this.plugin.startBackgroundManager(); // 重新启动背景管理器以应用更改
void this.plugin.saveSettings();
this.display(this.container);
},
}).open();
});
})
.addButton((button) => {
button.setButtonText(t("reset_time_rules_button"));
button.setTooltip(t("reset_time_rules_tooltip"));
button.onClick(() => {
this.plugin.settings.timeRules = this.defaultSettings.timeRules;
this.plugin.startBackgroundManager(); // 重新启动背景管理器以应用更改
void this.plugin.saveSettings();
this.display(this.container);
});
});
// 添加时间规则提示
const hint = containerEl.createDiv("dtb-hint");
hint.textContent = t("time_rule_hint");
// 显示时间规则列表
this.timeRulesContainer = containerEl.createDiv("dtb-section-container");
this.displayTimeRules();
} else {
this.timeRulesContainer = null;
}
// 时间间隔设置(仅在间隔模式下显示)
if (this.plugin.settings.mode === "interval") {
new Setting(containerEl)
.setName(t("interval_name"))
.setDesc(t("interval_desc"))
.addText((text) =>
text
.setPlaceholder("60")
.setValue(this.plugin.settings.intervalMinutes.toString())
.onChange(async (value) => {
const minutes = parseInt(value) || 60;
this.plugin.settings.intervalMinutes = minutes;
await this.plugin.saveSettings();
this.plugin.startBackgroundManager();
})
);
// 随机壁纸设置
new Setting(containerEl)
.setName(t("enable_random_wallpaper_name"))
.setDesc(t("enable_random_wallpaper_desc"))
.addToggle((toggle) =>
toggle.setValue(this.plugin.settings.enableRandomWallpaper).onChange(async (value) => {
this.plugin.settings.enableRandomWallpaper = value;
await this.plugin.saveSettings();
this.display(this.container);
})
);
}
}
// 显示时间规则列表,支持编辑、删除和添加新规则
displayTimeRules(): void {
const container = this.timeRulesContainer;
if (!container) return;
this.timeRuleDragSort?.disableAllDrag();
container.empty();
// 初始化时间规则拖拽排序
this.timeRuleDragSort = new DragSort<TimeRule>({
container,
items: this.plugin.settings.timeRules,
getItemId: (rule) => rule.id,
itemClass: "dtb-draggable",
idDataAttribute: "ruleId",
onReorder: async (reorderedRules) => {
this.plugin.settings.timeRules = reorderedRules;
await this.plugin.saveSettings();
this.displayTimeRules();
},
});
// 获取当前激活的时间规则
const activeRule = this.plugin.getCurrentTimeRule();
this.plugin.settings.timeRules.forEach((rule: TimeRule) => {
const setting = new Setting(container);
setting.setName(rule.name).setDesc(`${rule.startTime} - ${rule.endTime}`);
// 如果是激活的时间规则,则添加一个提示图标
if (rule.id === activeRule?.id) {
const indicator = setting.controlEl.createDiv();
indicator.setText("🔥");
indicator.title = t("active_time_rule");
}
setting
.addToggle((toggle) =>
toggle.setValue(rule.enabled).onChange(async (value) => {
rule.enabled = value;
this.plugin.startBackgroundManager(); // 重新启动背景管理器以应用更改
await this.plugin.saveSettings();
this.displayTimeRules();
})
)
.addDropdown((dropdown) => {
dropdown.addOption("", t("select_background_option"));
this.plugin.settings.backgrounds.forEach((bg) => {
dropdown.addOption(bg.id, bg.name);
});
// 使用增强版 tooltip 方法为背景选择下拉框添加动态提示
addEnhancedDropdownTooltip(dropdown, {
defaultTooltip: t("select_background_option"),
showSelectedValue: true,
customFormatter: (value, text) => {
if (!value) return t("select_background_option");
const bg = this.plugin.settings.backgrounds.find((b) => b.id === value);
if (bg) {
return `${text} (${bg.type.toUpperCase()})`;
}
return text;
},
});
return dropdown.setValue(rule.backgroundId).onChange((value) => {
rule.backgroundId = value;
void this.plugin.saveSettings();
void this.plugin.updateBackground(true);
});
})
.addButton((button) =>
button.setButtonText(t("button_edit")).onClick(() => this.showTimeRuleModal(rule))
)
.addButton((button) =>
button.setButtonText(t("button_delete")).onClick(() => {
this.plugin.settings.timeRules = this.plugin.settings.timeRules.filter((r) => r.id !== rule.id);
this.plugin.startBackgroundManager(); // 重新启动背景管理器以应用更改
void this.plugin.saveSettings();
this.displayTimeRules();
})
);
// 设置拖拽属性
setting.settingEl.addClass("dtb-draggable");
setting.settingEl.dataset.ruleId = rule.id;
// 添加通用条目样式类
setting.settingEl.addClass("dtb-button-container"); // 按钮样式
// 启用拖拽功能
this.timeRuleDragSort?.enableDragForElement(setting.settingEl, rule);
});
}
// 显示添加或编辑时间规则的模态窗口
private showTimeRuleModal(rule?: TimeRule) {
// 如果没有提供规则,创建一个新的空规则
const editRule: TimeRule = rule ?? {
id: "",
name: "",
startTime: "09:00",
endTime: "17:00",
backgroundId: "",
enabled: true,
};
const modal = new TimeRuleModal(this.plugin.app, editRule, (updatedRule) => {
if (!updatedRule.name.trim() || !updatedRule.startTime || !updatedRule.endTime) {
new Notice(t("notice_all_fields_required"));
return;
}
if (rule) {
// 编辑现有规则
const index = this.plugin.settings.timeRules.findIndex((r) => r.id === rule.id);
if (index !== -1) {
this.plugin.settings.timeRules[index] = {
...rule,
name: updatedRule.name.trim(),
startTime: updatedRule.startTime,
endTime: updatedRule.endTime,
};
}
} else {
// 添加新规则
const newRule: TimeRule = {
id: generateId("rule"),
name: updatedRule.name.trim(),
startTime: updatedRule.startTime,
endTime: updatedRule.endTime,
backgroundId: this.plugin.settings.backgrounds[0]?.id ?? "",
enabled: true,
};
this.plugin.settings.timeRules.push(newRule);
}
this.plugin.startBackgroundManager(); // 重新启动背景管理器以应用更改
void this.plugin.saveSettings();
this.displayTimeRules();
});
modal.open();
}
cleanup(): void {
this.timeRuleDragSort?.disableAllDrag();
}
}

File diff suppressed because it is too large Load diff

View file

@ -47,16 +47,16 @@ export class DTBSettingsView extends ItemView {
}
async onClose(): Promise<void> {
// 清理设置标签页的订阅
if (this.settingTab) {
this.settingTab.cleanup();
}
// hide() 调用 cleanup() + 设置 active=false + 从 settingTabs Map 中移除
this.settingTab.hide();
// 清理资源
if (this.settingTab && this.settingTab.containerEl) {
this.settingTab.containerEl.empty();
// 清理 DOM
if (this.settingTab.containerEl) {
this.settingTab.containerEl.empty();
}
}
this.settingTab = null; // 释放引用,帮助垃圾回收
this.settingTab = null;
return Promise.resolve();
}
}

View file

@ -3,6 +3,8 @@
*
*/
import { logger } from "../core/logger";
/**
*
*/
@ -80,11 +82,10 @@ export class DragSort<T> {
element.classList.remove(this.config.itemClass);
}
// 移除事件监听器
// 移除事件监听器WeakMap 自动清理引用,无需手动 delete
const cleanup = this.dragHandles.get(element);
if (cleanup) {
cleanup();
this.dragHandles.delete(element);
}
}
@ -205,7 +206,7 @@ export class DragSort<T> {
const targetIndex = items.findIndex((item) => this.config.getItemId(item) === targetId);
if (draggedIndex === -1 || targetIndex === -1) {
console.warn("DTB: Invalid drag operation - item not found");
logger.warn("Invalid drag operation - item not found");
return;
}

View file

@ -3,6 +3,8 @@
* @description Utility functions for the Obsidian Dynamic Theme Background plugin.
*/
import { logger } from "../core/logger";
/**
* Converts a hexadecimal color string to an RGBA color string with the specified opacity.
*
@ -26,7 +28,7 @@ export function hexToRgba(hex: string, opacity: number): string {
}
if (hex.length !== 6) {
console.warn("DTB: Invalid hex color format:", hex);
logger.warn("Invalid hex color format:", hex);
return `rgba(31, 30, 30, ${opacity})`;
}
@ -36,3 +38,12 @@ export function hexToRgba(hex: string, opacity: number): string {
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
}
/**
* ID
* @param prefix
*/
export function generateId(prefix?: string): string {
const uuid = crypto.randomUUID();
return prefix ? `${prefix}-${uuid}` : uuid;
}

View file

@ -1,3 +1,5 @@
import { logger } from "../../core/logger";
import { generateId } from "../../utils/utils";
import { ApiError, ApiErrorType } from "./api-error";
import { apiRegistry } from "./api-registry";
import { ApiStateManager } from "./api-state-manager";
@ -44,7 +46,7 @@ class WallpaperApiManager {
`Invalid parameters for API "${config.name}": ${validation.errors?.join(", ") ?? "Unknown error"}`,
config.id
);
console.warn(`WallpaperApiManager: ${error.message}`);
logger.warn(error.message);
return;
}
@ -52,7 +54,7 @@ class WallpaperApiManager {
const ApiClass = apiRegistry.getApiClass(config.type);
if (!ApiClass) {
console.warn(`WallpaperApiManager: Unsupported API type "${config.type}". No registered API class found.`);
logger.warn(`Unsupported API type "${config.type}". No registered API class found.`);
return;
}
@ -74,8 +76,11 @@ class WallpaperApiManager {
* @param apiId API实例唯一标识符
*/
deleteApi(apiId: string): void {
const api = this.apis.get(apiId);
if (api) {
void api.tryDisable(); // deinit 后再移除
}
this.apis.delete(apiId);
// 清理状态管理器中的订阅
this.stateManager.cleanupByApiId(apiId);
}
@ -83,8 +88,10 @@ class WallpaperApiManager {
* API实例
*/
deleteAllApis(): void {
for (const api of this.apis.values()) {
void api.tryDisable();
}
this.apis.clear();
// 清理状态管理器中的所有订阅
this.stateManager.cleanup();
}
@ -131,7 +138,7 @@ class WallpaperApiManager {
`API instance with ID "${apiId}" not found`,
apiId
);
console.warn(`WallpaperApiManager: ${error.message}`);
logger.warn(error.message);
this.stateManager.notify(apiId, {
configEnabled: false,
instanceEnabled: false,
@ -163,7 +170,7 @@ class WallpaperApiManager {
});
apiConfig.enabled = false;
console.warn(`WallpaperApiManager: Failed to enable API "${apiId}", config reverted.`);
logger.warn(`Failed to enable API "${apiId}", config reverted.`);
return false;
}
@ -198,7 +205,7 @@ class WallpaperApiManager {
});
apiConfig.enabled = false;
console.error(`WallpaperApiManager: Error enabling API "${apiId}":`, error);
logger.error(`Error enabling API "${apiId}":`, error);
return false;
}
}
@ -212,7 +219,7 @@ class WallpaperApiManager {
// 1. 先找到配置和API实例
const api = this.getApiById(apiId);
if (!api) {
console.warn(`WallpaperApiManager: API config or instance with ID "${apiId}" not found.`);
logger.warn(`API config or instance with ID "${apiId}" not found.`);
return false;
}
@ -240,7 +247,7 @@ class WallpaperApiManager {
if (apiId) {
const foundApi = this.getApiById(apiId);
if (!foundApi) {
console.warn(`WallpaperApiManager: API with ID "${apiId}" not found.`);
logger.warn(`API with ID "${apiId}" not found.`);
return null;
}
randomApi = foundApi;
@ -248,7 +255,7 @@ class WallpaperApiManager {
// 随机从已启用的API中选择一个
const enabledApis = Array.from(this.apis.values()).filter((api) => api.getEnabled());
if (enabledApis.length === 0) {
console.warn("No enabled wallpaper APIs available.");
logger.warn("No enabled wallpaper APIs available.");
return null;
}
randomApi = enabledApis[Math.floor(Math.random() * enabledApis.length)];
@ -257,7 +264,7 @@ class WallpaperApiManager {
// 获取壁纸
const imageList = await randomApi.getImages(count);
if (!imageList || imageList.length === 0) {
console.warn(`No images found for API: ${randomApi.getName()}`);
logger.warn(`No images found for API: ${randomApi.getName()}`);
return null;
}
return imageList;
@ -268,7 +275,7 @@ class WallpaperApiManager {
// ============================================================================
private genApiId(): string {
return `api-${crypto.randomUUID()}`;
return generateId("api");
}
}

View file

@ -3,6 +3,7 @@
* 使
*/
import { logger } from "../../core/logger";
import type { BaseWallpaperApi } from "./base-api";
import type {
WallpaperApiConfig,
@ -70,7 +71,7 @@ class ApiRegistry {
createInstance(type: WallpaperApiType, config: WallpaperApiConfig): BaseWallpaperApi | null {
const ApiClass = this.apiClasses.get(type);
if (!ApiClass) {
console.warn(`API type "${type}" is not registered`);
logger.warn(`API type "${type}" is not registered`);
return null;
}
return new ApiClass(config);

View file

@ -3,6 +3,8 @@
* API的配置状态UI状态之间的同步
*/
import { logger } from "../../core/logger";
/**
* API状态订阅者
*/
@ -66,7 +68,7 @@ export class ApiStateManager {
await result;
}
} catch (error) {
console.warn(`DTB: Error in state change callback for ${subscriber.name}:`, error);
logger.warn(`Error in state change callback for ${subscriber.name}:`, error);
}
});
@ -77,7 +79,7 @@ export class ApiStateManager {
// 并发执行所有回调,但不等待结果(火后即忘模式)
if (callbackPromises.length > 0) {
void Promise.all(callbackPromises).catch((error) => {
console.warn("DTB: Unexpected error in state notification batch:", error);
logger.warn("Unexpected error in state notification batch:", error);
});
}
}, 0);

View file

@ -3,6 +3,8 @@
*
*/
import { logger } from "../../core/logger";
import { generateId } from "../../utils/utils";
import type {
WallpaperApiConfig,
WallpaperApiEndpoints,
@ -66,12 +68,56 @@ export abstract class BaseWallpaperApi {
}
// ============================================================================
// 抽象方法 - 子类必须实现
// 抽象/模板方法
// ============================================================================
abstract init(): Promise<boolean>; // 启用插件时必须调用
abstract deinit(): Promise<boolean>; // 禁用插件时必须调用
abstract updateImageCache(): Promise<boolean>; // 更新图片缓存数据 wallpaperImageCache
/**
*
*/
deinit(): Promise<boolean> {
if (!this.initialized) return Promise.resolve(true);
this.wallpaperImageCache = [];
this.curDataIndex = 0;
this.currentPage = 1;
this.initialized = false;
return Promise.resolve(true);
}
/**
*
* fetchPage()
* qihoo360custom
*/
async updateImageCache(): Promise<boolean> {
if (this.totalPages > 0 && this.currentPage > this.totalPages) {
this.currentPage = 1;
}
const success = await this.fetchPage(this.currentPage);
if (success) {
this.currentPage += 1;
}
return success;
}
/**
*
* fetchPage updateImageCache
*/
protected fetchPage(_page: number): Promise<boolean> {
throw new Error("fetchPage must be implemented by subclass or override updateImageCache");
}
/**
*
*/
protected finishInit(): void {
this.wallpaperImageCache = [];
this.curDataIndex = 0;
this.currentPage = 1;
this.initialized = true;
}
// ============================================================================
// 静态方法 - 基类中抛出错误的方法,子类必须实现
@ -169,7 +215,7 @@ export abstract class BaseWallpaperApi {
}
return success;
} catch (error) {
console.error(`Failed to enable API "${this.name}":`, error);
logger.error(`Failed to enable API "${this.name}":`, error);
return false;
}
}
@ -186,7 +232,7 @@ export abstract class BaseWallpaperApi {
}
return success;
} catch (error) {
console.error(`Failed to disable API "${this.name}":`, error);
logger.error(`Failed to disable API "${this.name}":`, error);
return false;
}
}
@ -196,13 +242,29 @@ export abstract class BaseWallpaperApi {
// ============================================================================
generateBackgroundId(): string {
return `${this.getId()}-${Date.now()}`;
return generateId(this.getId());
}
generateBackgroundName(): string {
return `${this.getName()} - ${new Date().toLocaleString()}`;
}
/**
*
*/
protected safeString(value: unknown): string {
if (value === null || value === undefined) {
return "";
}
if (typeof value === "string") {
return value;
}
if (typeof value === "number" || typeof value === "boolean") {
return String(value);
}
return "";
}
protected saveConfig(): void {
// 将 api 的配置同步过到 config 中
this.config.baseUrl = this.baseUrl;
@ -282,9 +344,9 @@ export abstract class BaseWallpaperApi {
}
if (images.length < imageNum) {
await this.updateImageCache();
if (this.wallpaperImageCache.length === 0) {
break; // 没有更多图片
const updated = await this.updateImageCache();
if (!updated || this.wallpaperImageCache.length === 0) {
break; // 获取失败或没有更多图片
}
this.curDataIndex = 0;
}

View file

@ -4,6 +4,8 @@
import { JSONPath } from "jsonpath-plus";
import { requestUrl } from "obsidian";
import { logger } from "../../core/logger";
import { generateId } from "../../utils/utils";
import {
apiRegistry,
BaseWallpaperApi,
@ -71,13 +73,13 @@ export class CustomApi extends BaseWallpaperApi {
validateParams(_params: WallpaperApiParams): boolean {
if (!this.baseUrl) {
console.warn("Custom API: baseUrl is required");
logger.warn("Custom API: baseUrl is required");
return false;
}
const urlJsonPath = this.config.customSettings?.imageUrlJsonPath;
if (!urlJsonPath) {
console.warn("Custom API: imageUrlJsonPath is required");
logger.warn("Custom API: imageUrlJsonPath is required");
return false;
}
@ -97,37 +99,24 @@ export class CustomApi extends BaseWallpaperApi {
// 测试连通性
const images = await this.fetchImages();
if (!images || images.length === 0) {
console.warn("Custom API initialization failed: No images returned.");
logger.warn("Custom API initialization failed: No images returned.");
return false;
}
// 初始化数据缓存
this.wallpaperImageCache = [];
this.curDataIndex = 0;
this.currentPage = 1;
this.finishInit();
this.totalPages = 1; // Custom API 通常只有一页
this.totalCount = images.length;
this.initialized = true;
return true;
} catch (error) {
console.error("Custom API initialization failed:", error);
logger.error("Custom API initialization failed:", error);
return false;
}
}
deinit(): Promise<boolean> {
if (!this.initialized) {
return Promise.resolve(true);
}
// 清理缓存数据
this.wallpaperImageCache = [];
this.curDataIndex = 0;
this.currentPage = 1;
this.initialized = false;
return Promise.resolve(true);
return super.deinit(); // 重置 cache, curDataIndex, currentPage, initialized
}
async updateImageCache(): Promise<boolean> {
@ -141,7 +130,7 @@ export class CustomApi extends BaseWallpaperApi {
}
return false;
} catch (error) {
console.error("Custom API cache update failed:", error);
logger.error("Custom API cache update failed:", error);
return false;
}
}
@ -169,7 +158,7 @@ export class CustomApi extends BaseWallpaperApi {
const data = response.json;
return this.transformCustomResponse(data);
} catch (error) {
console.error("Custom API fetch error:", error);
logger.error("Custom API fetch error:", error);
throw error;
}
}
@ -184,7 +173,7 @@ export class CustomApi extends BaseWallpaperApi {
const urlJsonPath = this.config.customSettings?.imageUrlJsonPath;
if (!urlJsonPath) {
console.warn("Custom API: imageUrlJsonPath is required for JSON response");
logger.warn("Custom API: imageUrlJsonPath is required for JSON response");
return [];
}
@ -196,7 +185,7 @@ export class CustomApi extends BaseWallpaperApi {
});
if (!urls || (Array.isArray(urls) && urls.length === 0)) {
console.warn("Custom API: No URLs found at path:", urlJsonPath);
logger.warn("Custom API: No URLs found at path:", urlJsonPath);
return [];
}
@ -213,15 +202,14 @@ export class CustomApi extends BaseWallpaperApi {
};
});
} catch (error) {
console.warn("Error parsing custom API response:", error);
logger.warn("Error parsing custom API response:", error);
return [];
}
}
// 生成图片ID
private generateImageId(index?: number): string {
const timestamp = Date.now();
return `custom_${timestamp}_${index !== undefined ? index : Math.random().toString(36).substring(2, 9)}`;
private generateImageId(_index?: number): string {
return generateId("custom");
}
}

View file

@ -3,6 +3,7 @@
*/
import { Notice, requestUrl } from "obsidian";
import { logger } from "../../core/logger";
import { t } from "../../i18n";
import {
apiRegistry,
@ -221,14 +222,14 @@ export class PexelsApi extends BaseWallpaperApi {
}
if (!response || !response.photos || !Array.isArray(response.photos)) {
console.warn("Pexels API initialization failed: Invalid response format.");
logger.warn("Pexels API initialization failed: Invalid response format.");
return false;
}
// 更新分页信息
// 更新分页信息(先更新 perPage再计算 totalPages
this.perPage = Number(this.params.per_page) || 15;
this.totalPages = response.total_results ? Math.ceil(response.total_results / this.perPage) : -1;
this.totalCount = response.total_results ?? -1;
this.perPage = Number(this.params.per_page) || 15;
new Notice(
t("api_initialized_notice", {
@ -238,65 +239,34 @@ export class PexelsApi extends BaseWallpaperApi {
})
);
// 初始化数据缓存
this.wallpaperImageCache = [];
this.curDataIndex = 0;
this.currentPage = 1;
this.initialized = true;
this.finishInit();
return true;
} catch (error) {
console.warn("Pexels API initialization failed:", error);
logger.warn("Pexels API initialization failed:", error);
return false;
}
}
deinit(): Promise<boolean> {
if (!this.initialized) {
return Promise.resolve(true);
}
// 清理缓存数据
this.wallpaperImageCache = [];
this.curDataIndex = 0;
this.currentPage = 1;
this.initialized = false;
return Promise.resolve(true);
}
async updateImageCache(): Promise<boolean> {
// 如果已经到最后一页了,跳到第一页
if (this.totalPages > 0 && this.currentPage > this.totalPages) {
this.currentPage = 1;
}
let success = false;
if (this.params.query) {
// 有搜索查询,使用搜索接口
success = await this.fetchAndCacheSearchResults(this.currentPage);
} else {
// 没有搜索查询,使用精选接口
success = await this.fetchAndCacheCuratedPhotos(this.currentPage);
}
if (success) {
this.currentPage += 1;
}
return success;
}
// ============================================================================
// 辅助方法
// ============================================================================
protected async fetchPage(page: number): Promise<boolean> {
const query = this.params.query as string | undefined;
if (query && query.trim() !== "") {
return this.fetchAndCacheSearchResults(page);
} else {
return this.fetchAndCacheCuratedPhotos(page);
}
}
// 拉取搜索结果并缓存
private async fetchAndCacheSearchResults(page = this.currentPage): Promise<boolean> {
try {
const data = await this.fetchSearchResults(page);
if (!data || !data.photos || !Array.isArray(data.photos)) {
console.warn("Invalid search response format from Pexels API");
logger.warn("Invalid search response format from Pexels API");
return false;
}
@ -310,7 +280,7 @@ export class PexelsApi extends BaseWallpaperApi {
return true;
} catch (error) {
console.warn(`Error fetching search results from Pexels API:`, error);
logger.warn("Error fetching search results from Pexels API:", error);
return false;
}
}
@ -320,8 +290,9 @@ export class PexelsApi extends BaseWallpaperApi {
try {
const data = await this.fetchCuratedPhotos(page);
if (!data || !data.photos || !Array.isArray(data.photos)) {
console.warn("Invalid curated photos response format from Pexels API");
if (!data || !data.photos || !Array.isArray(data.photos) || data.photos.length === 0) {
logger.warn("Invalid or empty curated photos response from Pexels API");
this.currentPage = 1; // 重置页码以便下次从头获取
return false;
}
@ -331,7 +302,7 @@ export class PexelsApi extends BaseWallpaperApi {
return true;
} catch (error) {
console.warn(`Error fetching curated photos from Pexels API:`, error);
logger.warn("Error fetching curated photos from Pexels API:", error);
return false;
}
}
@ -377,7 +348,7 @@ export class PexelsApi extends BaseWallpaperApi {
queryParams.append("per_page", String(this.params.per_page || this.perPage));
const url = `${this.buildEndpointUrl("curated")}?${queryParams.toString()}`;
console.debug(`Fetching Pixabay curated photos from: ${url}`);
logger.debug(`Fetching Pexels curated photos from: ${url}`);
const response = await requestUrl({
url,
headers: {
@ -387,20 +358,6 @@ export class PexelsApi extends BaseWallpaperApi {
return response.json;
}
// 安全地将未知值转换为字符串,避免对象被转换为 [object Object]
private safeString(value: unknown): string {
if (value === null || value === undefined) {
return "";
}
if (typeof value === "string") {
return value;
}
if (typeof value === "number" || typeof value === "boolean") {
return String(value);
}
return "";
}
// 辅助方法:转换 API 返回的图片数据为 WallpaperImage
private transformPhoto(photo: Record<string, unknown>): WallpaperImage {
const src = (photo.src as Record<string, unknown>) ?? {};

View file

@ -3,6 +3,7 @@
*/
import { Notice, requestUrl } from "obsidian";
import { logger } from "../../core/logger";
import { t } from "../../i18n";
import {
apiRegistry,
@ -303,14 +304,14 @@ export class PixabayApi extends BaseWallpaperApi {
const response = await this.fetchSearchResults(1);
if (!response || !response.hits || !Array.isArray(response.hits)) {
console.warn("Pixabay API initialization failed: Invalid response format.");
logger.warn("Pixabay API initialization failed: Invalid response format.");
return false;
}
// 更新分页信息
// 更新分页信息(先更新 perPage再计算 totalPages
this.perPage = Number(this.params.per_page) || 20;
this.totalPages = response.totalHits ? Math.ceil(response.totalHits / this.perPage) : -1;
this.totalCount = response.total ?? -1;
this.perPage = Number(this.params.per_page) || 20;
new Notice(
t("api_initialized_notice", {
@ -320,57 +321,24 @@ export class PixabayApi extends BaseWallpaperApi {
})
);
// 初始化数据缓存
this.wallpaperImageCache = [];
this.curDataIndex = 0;
this.currentPage = 1;
this.initialized = true;
this.finishInit();
return true;
} catch (error) {
console.warn("Pixabay API initialization failed:", error);
logger.warn("Pixabay API initialization failed:", error);
return false;
}
}
deinit(): Promise<boolean> {
if (!this.initialized) {
return Promise.resolve(true);
}
// 清理缓存数据
this.wallpaperImageCache = [];
this.curDataIndex = 0;
this.currentPage = 1;
this.initialized = false;
return Promise.resolve(true);
}
async updateImageCache(): Promise<boolean> {
// 如果已经到最后一页了,跳到第一页
if (this.totalPages > 0 && this.currentPage > this.totalPages) {
this.currentPage = 1;
}
const success = await this.fetchAndCacheSearchResults(this.currentPage);
if (success) {
this.currentPage += 1;
}
return success;
}
// ============================================================================
// 辅助方法
// ============================================================================
// 拉取搜索结果并缓存
private async fetchAndCacheSearchResults(page = this.currentPage): Promise<boolean> {
protected async fetchPage(page: number): Promise<boolean> {
try {
const data = await this.fetchSearchResults(page);
if (!data || !data.hits || !Array.isArray(data.hits)) {
console.warn("Invalid search response format from Pixabay API");
logger.warn("Invalid search response format from Pixabay API");
return false;
}
@ -384,7 +352,7 @@ export class PixabayApi extends BaseWallpaperApi {
return true;
} catch (error) {
console.warn(`Error fetching search results from Pixabay API:`, error);
logger.warn("Error fetching search results from Pixabay API:", error);
return false;
}
}
@ -434,25 +402,11 @@ export class PixabayApi extends BaseWallpaperApi {
}
const url = `${this.buildEndpointUrl("search")}?${queryParams.toString()}`;
console.debug(`Fetching Pixabay search results from: ${url}`);
logger.debug(`Fetching Pixabay search results from: ${url}`);
const response = await requestUrl({ url });
return response.json;
}
// 安全地将未知值转换为字符串,避免对象被转换为 [object Object]
private safeString(value: unknown): string {
if (value === null || value === undefined) {
return "";
}
if (typeof value === "string") {
return value;
}
if (typeof value === "number" || typeof value === "boolean") {
return String(value);
}
return "";
}
// 辅助方法:转换 API 返回的图片数据为 WallpaperImage
private transformImage(image: Record<string, unknown>): WallpaperImage {
const tagsStr = this.safeString(image.tags);

View file

@ -3,6 +3,8 @@
*/
import { Notice, requestUrl } from "obsidian";
import { logger } from "../../core/logger";
import { generateId } from "../../utils/utils";
import { t } from "../../i18n";
import {
apiRegistry,
@ -254,20 +256,19 @@ export class Qihoo360Api extends BaseWallpaperApi {
// 初始化时加载分类数据
await this.loadCategories();
await this.loadHotSearch();
this.initialized = true;
this.finishInit();
return true;
} catch (error) {
console.error("360壁纸API初始化失败:", error);
new Notice(t("notice_api_failed_enable_disable", { action: "启用", apiName: this.name }));
logger.error("360 API initialization failed:", error);
new Notice(t("notice_api_failed_enable_disable", { action: t("action_enable"), apiName: this.name }));
return false;
}
}
deinit(): Promise<boolean> {
this.initialized = false;
this.categoriesCache = [];
this.hotSearchCache = [];
return Promise.resolve(true);
return super.deinit(); // 重置 cache, curDataIndex, currentPage, initialized
}
async updateImageCache(): Promise<boolean> {
@ -289,11 +290,11 @@ export class Qihoo360Api extends BaseWallpaperApi {
this.curDataIndex = 0;
return true;
} else {
console.warn("360壁纸API: 未获取到壁纸数据");
logger.warn("360壁纸API: 未获取到壁纸数据");
return false;
}
} catch (error) {
console.error("360壁纸API更新缓存失败:", error);
logger.error("360壁纸API更新缓存失败:", error);
new Notice(t("notice_api_failed_fetch", { apiName: this.name }));
return false;
}
@ -309,7 +310,7 @@ export class Qihoo360Api extends BaseWallpaperApi {
private async loadCategories(): Promise<void> {
try {
const url = this.buildEndpointUrl("categories");
console.debug("360壁纸API: 获取分类列表", url);
logger.debug("360壁纸API: 获取分类列表", url);
const response = await requestUrl({
url: url,
@ -320,12 +321,12 @@ export class Qihoo360Api extends BaseWallpaperApi {
if (data.errno === "0" && data.data) {
this.categoriesCache = data.data;
console.debug(`360壁纸API: 成功加载${data.data.length}个分类`);
logger.debug(`360壁纸API: 成功加载${data.data.length}个分类`);
} else {
throw new Error(`API返回错误: ${data.errmsg}`);
}
} catch (error) {
console.error("360壁纸API: 加载分类失败", error);
logger.error("360壁纸API: 加载分类失败", error);
throw error;
}
}
@ -340,7 +341,7 @@ export class Qihoo360Api extends BaseWallpaperApi {
if (!url) {
throw new Error("hotSearch endpoint not configured");
}
console.debug("360壁纸API: 获取热门搜索", url);
logger.debug("360壁纸API: 获取热门搜索", url);
const response = await requestUrl({
url: url,
@ -351,10 +352,10 @@ export class Qihoo360Api extends BaseWallpaperApi {
if (data.error === 0 && data.data) {
this.hotSearchCache = data.data;
console.debug(`360壁纸API: 成功加载${data.data.length}个热门搜索词`);
logger.debug(`360壁纸API: 成功加载${data.data.length}个热门搜索词`);
}
} catch (error) {
console.error("360壁纸API: 加载热门搜索失败", error);
logger.error("360壁纸API: 加载热门搜索失败", error);
// 热门搜索加载失败不影响主要功能
}
}
@ -373,7 +374,7 @@ export class Qihoo360Api extends BaseWallpaperApi {
});
const url = `${this.baseUrl}/index.php?${params.toString()}`;
console.debug("360壁纸API: 获取最新壁纸", url);
logger.debug("360壁纸API: 获取最新壁纸", url);
const response = await requestUrl({
url: url,
@ -403,7 +404,7 @@ export class Qihoo360Api extends BaseWallpaperApi {
});
const url = `${this.baseUrl}/index.php?${params.toString()}`;
console.debug("360壁纸API: 获取分类壁纸", url);
logger.debug("360壁纸API: 获取分类壁纸", url);
const response = await requestUrl({
url: url,
@ -438,7 +439,7 @@ export class Qihoo360Api extends BaseWallpaperApi {
});
const url = `${this.baseUrl}/index.php?${params.toString()}`;
console.debug("360壁纸API: 搜索壁纸", url);
logger.debug("360壁纸API: 搜索壁纸", url);
const response = await requestUrl({
url: url,
@ -457,7 +458,7 @@ export class Qihoo360Api extends BaseWallpaperApi {
/**
* API响应为WallpaperImage格式
*/
private convertToWallpaperImage(item: Qihoo360WallpaperResponse["data"][number], index: number): WallpaperImage {
private convertToWallpaperImage(item: Qihoo360WallpaperResponse["data"][number], _index: number): WallpaperImage {
// 获取首选分辨率设置
const preferredResolution = this.params.preferredResolution || "auto";
let imageUrl = item.url;
@ -516,7 +517,7 @@ export class Qihoo360Api extends BaseWallpaperApi {
const categoryName = category ? category.name : `分类${item.cid}`;
return {
id: item.pid || item.id || `360-${Date.now()}-${index}`,
id: item.pid || item.id || generateId("360"),
url: imageUrl,
author: "360壁纸",
description: tags.length > 0 ? tags.join(", ") : categoryName,

View file

@ -3,6 +3,7 @@
*/
import { Notice, requestUrl } from "obsidian";
import { logger } from "../../core/logger";
import { t } from "../../i18n";
import {
apiRegistry,
@ -204,7 +205,7 @@ export class UnsplashApi extends BaseWallpaperApi {
// 测试API连通性 - 获取随机图片
const response = await this.fetchRandomPhotos(1);
if (!response || !Array.isArray(response) || response.length === 0) {
console.warn("Unsplash API initialization failed: No response from random endpoint.");
logger.warn("Unsplash API initialization failed: No response from random endpoint.");
return false;
}
@ -212,7 +213,7 @@ export class UnsplashApi extends BaseWallpaperApi {
if (this.params.query) {
const searchResp = await this.fetchSearchResults(1);
if (!searchResp) {
console.warn("Unsplash API initialization failed: No response from search endpoint.");
logger.warn("Unsplash API initialization failed: No response from search endpoint.");
return false;
}
this.totalPages = searchResp.total_pages ?? -1;
@ -233,62 +234,34 @@ export class UnsplashApi extends BaseWallpaperApi {
this.perPage = Number(this.params.per_page) || 10;
// 初始化数据缓存
this.wallpaperImageCache = [];
this.curDataIndex = 0;
this.currentPage = 1;
this.initialized = true;
this.finishInit();
return true;
} catch (error) {
console.warn("Unsplash API initialization failed:", error);
logger.warn("Unsplash API initialization failed:", error);
return false;
}
}
deinit(): Promise<boolean> {
if (!this.initialized) {
return Promise.resolve(true);
}
// 清理缓存数据
this.wallpaperImageCache = [];
this.curDataIndex = 0;
this.currentPage = 1;
this.initialized = false;
return Promise.resolve(true);
}
async updateImageCache(): Promise<boolean> {
// 如果有搜索查询,使用搜索接口
if (this.params.query) {
// 如果已经到最后一页了,跳到第一页
if (this.totalPages > 0 && this.currentPage > this.totalPages) {
this.currentPage = 1;
}
const success = await this.fetchAndCacheSearchResults(this.currentPage);
if (success) {
this.currentPage += 1;
}
return success;
} else {
// 没有搜索查询,使用随机图片接口
return await this.fetchAndCacheRandomPhotos();
}
}
// ============================================================================
// 辅助方法
// ============================================================================
protected async fetchPage(page: number): Promise<boolean> {
const query = this.params.query as string | undefined;
if (query && query.trim() !== "") {
return this.fetchAndCacheSearchResults(page);
} else {
return this.fetchAndCacheRandomPhotos();
}
}
// 拉取搜索结果并缓存
private async fetchAndCacheSearchResults(page = this.currentPage): Promise<boolean> {
try {
const data = await this.fetchSearchResults(page);
if (!data || !data.results || !Array.isArray(data.results)) {
console.warn("Invalid search response format from Unsplash API");
logger.warn("Invalid search response format from Unsplash API");
return false;
}
@ -302,7 +275,7 @@ export class UnsplashApi extends BaseWallpaperApi {
return true;
} catch (error) {
console.warn(`Error fetching search results from Unsplash API:`, error);
logger.warn("Error fetching search results from Unsplash API:", error);
return false;
}
}
@ -313,7 +286,7 @@ export class UnsplashApi extends BaseWallpaperApi {
const photos = await this.fetchRandomPhotos(this.perPage);
if (!photos || !Array.isArray(photos)) {
console.warn("Invalid random photos response format from Unsplash API");
logger.warn("Invalid random photos response format from Unsplash API");
return false;
}
@ -323,7 +296,7 @@ export class UnsplashApi extends BaseWallpaperApi {
return true;
} catch (error) {
console.warn(`Error fetching random photos from Unsplash API:`, error);
logger.warn("Error fetching random photos from Unsplash API:", error);
return false;
}
}
@ -356,7 +329,7 @@ export class UnsplashApi extends BaseWallpaperApi {
}
const url = `${this.buildEndpointUrl("search")}?${queryParams.toString()}`;
console.debug(`Fetching Unsplash search results from: ${url}`);
logger.debug(`Fetching Unsplash search results from: ${url}`);
const response = await requestUrl({ url });
return response.json;
}
@ -384,25 +357,11 @@ export class UnsplashApi extends BaseWallpaperApi {
}
const url = `${this.buildEndpointUrl("random")}?${queryParams.toString()}`;
console.debug(`Fetching Unsplash random photos from: ${url}`);
logger.debug(`Fetching Unsplash random photos from: ${url}`);
const response = await requestUrl({ url });
return response.json;
}
// 安全地将未知值转换为字符串,避免对象被转换为 [object Object]
private safeString(value: unknown): string {
if (value === null || value === undefined) {
return "";
}
if (typeof value === "string") {
return value;
}
if (typeof value === "number" || typeof value === "boolean") {
return String(value);
}
return "";
}
// 辅助方法:转换 API 返回的图片数据为 WallpaperImage
private transformPhoto(photo: Record<string, unknown>): WallpaperImage {
const user = (photo.user as Record<string, unknown>) ?? {};

View file

@ -3,6 +3,7 @@
*/
import { Notice, requestUrl } from "obsidian";
import { logger } from "../../core/logger";
import { t } from "../../i18n";
import {
apiRegistry,
@ -89,14 +90,15 @@ export class WallhavenApi extends BaseWallpaperApi {
{ value: "anime", label: "Anime" },
{ value: "people", label: "People" },
],
toApiValue: (uiValue: string[]) => {
toApiValue: (uiValue) => {
const arr = Array.isArray(uiValue) ? uiValue : [String(uiValue)];
let result = "";
result += uiValue.includes("general") ? "1" : "0";
result += uiValue.includes("anime") ? "1" : "0";
result += uiValue.includes("people") ? "1" : "0";
return result ?? (defaultParams.categories as string);
result += arr.includes("general") ? "1" : "0";
result += arr.includes("anime") ? "1" : "0";
result += arr.includes("people") ? "1" : "0";
return result || (defaultParams.categories as string);
},
fromApiValue: (apiValue: string) => {
fromApiValue: (apiValue) => {
const str = apiValue?.toString() ?? (defaultParams.categories as string);
const result: string[] = [];
if (str[0] === "1") result.push("general");
@ -116,15 +118,15 @@ export class WallhavenApi extends BaseWallpaperApi {
{ value: "sketchy", label: "Sketchy" },
{ value: "nsfw", label: "NSFW (18+)" },
],
toApiValue: (uiValue: string[]) => {
const arr = Array.isArray(uiValue) ? uiValue : [];
toApiValue: (uiValue) => {
const arr = Array.isArray(uiValue) ? uiValue : [String(uiValue)];
let result = "";
result += arr.includes("sfw") ? "1" : "0";
result += arr.includes("sketchy") ? "1" : "0";
result += arr.includes("nsfw") ? "1" : "0";
return result || (defaultParams.purity as string);
},
fromApiValue: (apiValue: string) => {
fromApiValue: (apiValue) => {
const str = apiValue?.toString() ?? (defaultParams.purity as string);
const result: string[] = [];
if (str[0] === "1") result.push("sfw");
@ -291,21 +293,21 @@ export class WallhavenApi extends BaseWallpaperApi {
// 拉取第一页数据用于测试连通性
const resp = await this.fetchSearchResults(1);
if (!resp) {
console.warn("Wallhaven API initialization failed: No response from search endpoint.");
logger.warn("Wallhaven API initialization failed: No response from search endpoint.");
return false;
}
// 填充 totalPages、totalCount 等状态
const meta = resp.meta;
if (!meta) {
console.warn("Wallhaven API response missing meta information.");
logger.warn("Wallhaven API response missing meta information.");
return false;
}
this.totalPages = meta.last_page ?? -1;
this.totalCount = meta.total ?? -1;
this.perPage = meta.per_page ?? -1;
if (this.totalPages <= 0 || this.totalCount < 0 || this.perPage <= 0) {
console.warn("Wallhaven API response has invalid pagination data.");
logger.warn("Wallhaven API response has invalid pagination data.");
return false;
}
new Notice(
@ -316,59 +318,28 @@ export class WallhavenApi extends BaseWallpaperApi {
})
);
// 初始化数据缓存
this.wallpaperImageCache = [];
this.curDataIndex = 0;
this.currentPage = 1;
this.initialized = true;
this.finishInit();
return true;
}
deinit(): Promise<boolean> {
if (!this.initialized) {
return Promise.resolve(true);
}
// 清理缓存数据
this.wallpaperImageCache = [];
this.curDataIndex = 0;
this.currentPage = 1;
this.initialized = false;
return Promise.resolve(true);
}
async updateImageCache(): Promise<boolean> {
// 如果已经到最后一页了,跳到第一页
if (this.totalPages > 0 && this.currentPage > this.totalPages) {
this.currentPage = 1;
}
const success = await this.fetchAndCachePageImages(this.currentPage);
if (success) {
this.currentPage += 1;
}
return success;
}
// ============================================================================
// 辅助方法
// ============================================================================
// 拉取指定页码的壁纸数据并缓存到 wallpaperImageCache
private async fetchAndCachePageImages(page = this.currentPage): Promise<boolean> {
protected async fetchPage(page: number): Promise<boolean> {
try {
const data = await this.fetchSearchResults(page);
if (!data.data || !Array.isArray(data.data)) {
console.warn("Invalid API response format from Wallhaven");
logger.warn("Invalid API response format from Wallhaven");
return false;
}
// 为避免爆内存,这里应仅缓存当前页的数据
this.wallpaperImageCache = data.data.map((img: Record<string, unknown>) => this.transformImage(img));
this.curDataIndex = 0;
return true;
} catch (error) {
console.warn(`Error fetching images from Wallhaven API:`, error);
logger.warn("Error fetching images from Wallhaven API:", error);
return false;
}
}
@ -378,25 +349,11 @@ export class WallhavenApi extends BaseWallpaperApi {
// 合并参数并使用基类方法构建查询字符串
const allParams = { ...this.params, page };
const url = `${this.buildEndpointUrl("search")}?${this.buildUrlParams(allParams)}`;
console.debug(`Fetching Wallhaven search results from: ${url}`);
logger.debug(`Fetching Wallhaven search results from: ${url}`);
const response = await requestUrl({ url });
return response.json;
}
// 安全地将未知值转换为字符串,避免对象被转换为 [object Object]
private safeString(value: unknown): string {
if (value === null || value === undefined) {
return "";
}
if (typeof value === "string") {
return value;
}
if (typeof value === "number" || typeof value === "boolean") {
return String(value);
}
return "";
}
// 辅助方法:转换 API 返回的图片数据为 WallpaperImage
private transformImage(apiImage: Record<string, unknown>): WallpaperImage {
return {

View file

@ -5,12 +5,11 @@
"inlineSources": true,
"module": "ESNext",
"target": "ES6",
"strict": true,
"allowJs": true,
"noImplicitAny": true,
"moduleResolution": "node",
"importHelpers": true,
"isolatedModules": true,
"strictNullChecks": true,
"lib": [
"DOM",
"ES5",