diff --git a/package.json b/package.json index 969f502..7656317 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..2ec53c4 --- /dev/null +++ b/src/constants.ts @@ -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; diff --git a/src/core/background-manager.ts b/src/core/background-manager.ts new file mode 100644 index 0000000..a86f1f8 --- /dev/null +++ b/src/core/background-manager.ts @@ -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 { + 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 { + 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 { + 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; + } + } +} diff --git a/src/core/background-persistence.ts b/src/core/background-persistence.ts new file mode 100644 index 0000000..f7ae5cb --- /dev/null +++ b/src/core/background-persistence.ts @@ -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; + /** 更新后的 BackgroundItem(value 替换为本地路径, 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 { + 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) }; + } + } +} diff --git a/src/core/event-bus.ts b/src/core/event-bus.ts new file mode 100644 index 0000000..128d165 --- /dev/null +++ b/src/core/event-bus.ts @@ -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; + "backgrounds:changed": Record; + "apis:changed": Record; + "css:updated": Record; +} + +export type DTBEventKey = keyof DTBEventMap; + +type Listener = (payload: T) => void; + +/** + * 类型安全的事件总线 + */ +export class EventBus { + private listeners = new Map>>(); + + /** + * 订阅事件,返回取消订阅函数 + */ + on(event: K, cb: Listener): () => void { + if (!this.listeners.has(event)) { + this.listeners.set(event, new Set()); + } + const set = this.listeners.get(event)!; + set.add(cb as Listener); + + return () => { + set.delete(cb as Listener); + if (set.size === 0) { + this.listeners.delete(event); + } + }; + } + + /** + * 发送事件(同步通知所有监听器) + */ + emit(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(event: K, cb: Listener): void { + const set = this.listeners.get(event); + if (!set) return; + set.delete(cb as Listener); + if (set.size === 0) { + this.listeners.delete(event); + } + } + + /** + * 移除所有监听器 + */ + removeAllListeners(): void { + this.listeners.clear(); + } +} diff --git a/src/core/index.ts b/src/core/index.ts new file mode 100644 index 0000000..65af663 --- /dev/null +++ b/src/core/index.ts @@ -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"; diff --git a/src/core/logger.ts b/src/core/logger.ts new file mode 100644 index 0000000..f67ef82 --- /dev/null +++ b/src/core/logger.ts @@ -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), +}; diff --git a/src/core/style-manager.ts b/src/core/style-manager.ts new file mode 100644 index 0000000..3834bb6 --- /dev/null +++ b/src/core/style-manager.ts @@ -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(); + + 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 { + try { + const img = new Image(); + + await new Promise((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(); + } +} diff --git a/src/core/time-rule-scheduler.ts b/src/core/time-rule-scheduler.ts new file mode 100644 index 0000000..c00b46a --- /dev/null +++ b/src/core/time-rule-scheduler.ts @@ -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 }; + } +} diff --git a/src/modals/background-modal.ts b/src/modals/background-modal.ts index b4ccd04..eac57e4 100644 --- a/src/modals/background-modal.ts +++ b/src/modals/background-modal.ts @@ -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; diff --git a/src/modals/time-rule-modal.ts b/src/modals/time-rule-modal.ts index dd29611..79b8c1b 100644 --- a/src/modals/time-rule-modal.ts +++ b/src/modals/time-rule-modal.ts @@ -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, diff --git a/src/modals/wallpaper-api-modal.ts b/src/modals/wallpaper-api-modal.ts index ea0d445..24f590d 100644 --- a/src/modals/wallpaper-api-modal.ts +++ b/src/modals/wallpaper-api-modal.ts @@ -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 = new Map(); // 额外参数输入 - extraParamsTextarea: HTMLTextAreaElement; + extraParamsTextarea!: HTMLTextAreaElement; // 自定义设置容器的引用 - customSettingsSectionContainer: HTMLElement; + customSettingsSectionContainer!: HTMLElement; // 自定义设置输入元素 customSettingsInputs: Map = 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 })); } diff --git a/src/plugin.ts b/src/plugin.ts index 2ebcb29..e6c47d0 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -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 = new Map(); // 存储所有设置标签页的引用 + statusBar: HTMLElement | null = null; + settingTabs: Map = 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 { + 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((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 { - 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 { - 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 { - 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", {}); } } diff --git a/src/settings/sections/api-settings.ts b/src/settings/sections/api-settings.ts new file mode 100644 index 0000000..f21c649 --- /dev/null +++ b/src/settings/sections/api-settings.ts @@ -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; + + /** 外部回调:刷新背景列表 */ + 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({ + 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); + } +} diff --git a/src/settings/sections/basic-settings.ts b/src/settings/sections/basic-settings.ts new file mode 100644 index 0000000..509b845 --- /dev/null +++ b/src/settings/sections/basic-settings.ts @@ -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 + } +} diff --git a/src/settings/sections/bg-management.ts b/src/settings/sections/bg-management.ts new file mode 100644 index 0000000..e3d60d6 --- /dev/null +++ b/src/settings/sections/bg-management.ts @@ -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; + + /** 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({ + 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")); + } + } +} diff --git a/src/settings/sections/index.ts b/src/settings/sections/index.ts new file mode 100644 index 0000000..e25c01b --- /dev/null +++ b/src/settings/sections/index.ts @@ -0,0 +1,4 @@ +export { ApiSettingsSection } from "./api-settings"; +export { BasicSettingsSection } from "./basic-settings"; +export { BgManagementSection } from "./bg-management"; +export { ModeSettingsSection } from "./mode-settings"; diff --git a/src/settings/sections/mode-settings.ts b/src/settings/sections/mode-settings.ts new file mode 100644 index 0000000..c85b953 --- /dev/null +++ b/src/settings/sections/mode-settings.ts @@ -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 | 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({ + 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(); + } +} diff --git a/src/settings/settings-tab.ts b/src/settings/settings-tab.ts index 5cb4c0a..d7b581d 100644 --- a/src/settings/settings-tab.ts +++ b/src/settings/settings-tab.ts @@ -1,57 +1,35 @@ /** - * 设置标签页,用于显示和管理插件的设置 - * 包括基本设置、背景管理和模式设置等功能 + * 设置标签页协调器 + * 协调各设置区块的展示和刷新 */ -import { App, Notice, PluginSettingTab, Setting, TFile } from "obsidian"; +import { App, PluginSettingTab } from "obsidian"; + import { getDefaultSettings } from "../default-settings"; import { t } from "../i18n"; -import { - BackgroundModal, - ConfirmModal, - ImageFolderSuggestModal, - TimeRuleModal, - WallpaperApiEditorModal, -} from "../modals"; import type DynamicThemeBackgroundPlugin from "../plugin"; -import type { BackgroundItem, DTBSettings, TimeRule } from "../types"; -import { DragSort, addDropdownOptionHoverTooltip, addDropdownTooltip, addEnhancedDropdownTooltip } from "../utils"; +import type { DTBSettings } from "../types"; +import { generateId } from "../utils"; import { VERSION } from "../version"; -import { - ApiStateSubscriber, - BaseWallpaperApi, - WallpaperApiConfig, - WallpaperApiType, - apiManager, -} from "../wallpaper-apis"; +import { ApiSettingsSection, BasicSettingsSection, BgManagementSection, ModeSettingsSection } from "./sections"; export class DTBSettingTab extends PluginSettingTab { plugin: DynamicThemeBackgroundPlugin; defaultSettings: DTBSettings; - private componentId: string; // 组件唯一标识符 - private active: boolean; // 是否出于激活状态 + private componentId: string; + private active!: boolean; - // 记录一些 HTML 元素, 便于分块刷新 - // 注意这些值不应被在事件监听器、闭包、全局变量等持有 - private basicSettingEl: HTMLElement; - private modeSettingsEl: HTMLElement; - private timeRulesContainer: HTMLElement | null; // 某些情况下可能不存在 - private bgManagementEl: HTMLElement; - private bgListContainer: HTMLElement; - private wallpaperApiSettingsEl: HTMLElement; - private apiListContainer: HTMLElement; - - // 用于拖拽排序 - private backgroundDragSort?: DragSort; - private timeRuleDragSort?: DragSort; - private apiDragSort?: DragSort; + // 设置区块 + private basicSection?: BasicSettingsSection; + private modeSection?: ModeSettingsSection; + private bgSection?: BgManagementSection; + private apiSection?: ApiSettingsSection; constructor(app: App, plugin: DynamicThemeBackgroundPlugin) { super(app, plugin); this.plugin = plugin; this.defaultSettings = getDefaultSettings(); - // 生成唯一的组件ID - this.componentId = this.genComponentId(); + this.componentId = generateId("settings-tab"); // 注册到 plugin this.plugin.settingTabs.set(this.componentId, this); @@ -66,18 +44,13 @@ export class DTBSettingTab extends PluginSettingTab { // ============================================================================ hide(): void { - // 设置面板关闭时应清理所有订阅 this.cleanup(); - this.active = false; - // 取消 plugin 注册 this.plugin.settingTabs.delete(this.componentId); } display(): void { this.active = true; - - // 清理之前的订阅 this.cleanup(); const { containerEl } = this; @@ -85,26 +58,60 @@ export class DTBSettingTab extends PluginSettingTab { this.displayHeader(containerEl); - this.basicSettingEl = containerEl.createDiv(); // 这里适合使用默认的 div 容器,因为 displayXXX 内部会清理该容器 - this.displayBasicSettings(); + // 基础设置 + const basicEl = containerEl.createDiv(); + this.basicSection = new BasicSettingsSection(this.plugin, this.defaultSettings); + this.basicSection.display(basicEl); - this.modeSettingsEl = containerEl.createDiv(); // 这里适合使用默认的 div 容器,因为 displayXXX 内部会清理该容器 - this.displayModeSettings(); + // 模式设置 + const modeEl = containerEl.createDiv(); + this.modeSection = new ModeSettingsSection(this.plugin, this.defaultSettings); + this.modeSection.display(modeEl); - this.bgManagementEl = containerEl.createDiv(); // 这里适合使用默认的 div 容器,因为 displayXXX 内部会清理该容器 - this.displayBackgroundManagement(); + // 背景管理 + const bgEl = containerEl.createDiv(); + this.bgSection = new BgManagementSection(this.plugin, this.defaultSettings, { + onChanged: () => { + // 背景列表变化时也需要刷新时间规则下拉(因为时间规则引用背景 ID) + this.modeSection?.displayTimeRules(); + }, + }); + this.bgSection.display(bgEl); - this.wallpaperApiSettingsEl = containerEl.createDiv(); // 这里适合使用默认的 div 容器,因为 displayXXX 内部会清理该容器 - this.displayWallpaperApiSettings(); + // 壁纸 API 设置 + const apiEl = containerEl.createDiv(); + this.apiSection = new ApiSettingsSection(this.plugin, this.defaultSettings, { + onBackgroundsChanged: () => this.bgSection?.displayBackgrounds(), + onTimeRulesChanged: () => this.modeSection?.displayTimeRules(), + }); + this.apiSection.display(apiEl); } - // 显示设置页头 + // ============================================================================ + // 针对性刷新(plugin.ts 调用) + // ============================================================================ + + displayTimeRules(): void { + this.modeSection?.displayTimeRules(); + } + + displayBackgrounds(): void { + this.bgSection?.displayBackgrounds(); + } + + displayWallpaperApis(): void { + this.apiSection?.displayWallpaperApis(); + } + + // ============================================================================ + // 内部方法 + // ============================================================================ + private displayHeader(containerEl: HTMLElement) { const headerContainer = containerEl.createDiv("dtb-section-header"); // 创建左侧标题容器(预留位置) headerContainer.createDiv(); - // 移除顶级标题,按照Obsidian指南 // 创建右侧信息容器 const infoContainer = headerContainer.createDiv("dtb-links"); @@ -126,1290 +133,15 @@ export class DTBSettingTab extends PluginSettingTab { }); } - // ============================================================================ - // 基础设置 - // ============================================================================ - - displayBasicSettings() { - const containerEl = this.basicSettingEl; - 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.displayBasicSettings(); - }) - ); - - // 背景亮度设置 - 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.displayBasicSettings(); - }) - ); - - // 背景饱和度设置 - 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.displayBasicSettings(); - }) - ); - - // 背景颜色和透明度设置(暗/亮两套,同一行:暗在前,亮在后) - { - 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.displayBasicSettings(); - }) - ); - } - - // 背景填充方式设置 - 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: "cover" | "contain" | "auto" | "intelligent") => { - this.plugin.settings.bgSize = value; - 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.displayBasicSettings(); - }) - ); - } - - displayModeSettings() { - const containerEl = this.modeSettingsEl; - 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: "time-based" | "interval" | "manual") => { - this.plugin.settings.mode = value; - await this.plugin.saveSettings(); - this.plugin.startBackgroundManager(); - this.displayModeSettings(); - }); - // 添加模式切换的 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.app, { - message: t("confirm_clear_time_rules"), - onConfirm: () => { - this.plugin.settings.timeRules = []; - this.plugin.startBackgroundManager(); // 重新启动背景管理器以应用更改 - void this.plugin.saveSettings(); - this.displayModeSettings(); - }, - }).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.displayModeSettings(); - }); - }); - - // 添加时间规则提示 - 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.displayModeSettings(); - }) - ); - } - } - - // 显示时间规则列表,支持编辑、删除和添加新规则 - displayTimeRules(): void { - const container = this.timeRulesContainer; - if (!container) return; - container.empty(); - - // 初始化时间规则拖拽排序 - this.timeRuleDragSort = new DragSort({ - 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.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.name === rule.name); - if (index !== -1) { - this.plugin.settings.timeRules[index] = { - ...rule, - name: updatedRule.name.trim(), - startTime: updatedRule.startTime, - endTime: updatedRule.endTime, - }; - } - } else { - // 添加新规则 - const newRule: TimeRule = { - id: Date.now().toString() + Math.random().toString(36).substring(2, 9), - 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.plugin.refreshActiveTimeRules(); - }); - - modal.open(); - } - - // ============================================================================ - // 背景管理 - // ============================================================================ - - displayBackgroundManagement() { - const containerEl = this.bgManagementEl; - 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.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(); - } - - // 显示添加或编辑背景的模态窗口 - private showAddBackgroundModal(type: "image" | "color" | "gradient"): void { - // 创建一个新的背景项,初始值为空 - const bg: BackgroundItem = { - id: "", - name: "", - type, - value: "", - }; - const modal = new BackgroundModal(this.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 = Date.now().toString() + Math.random().toString(36).substring(2, 9); - - // 添加到设置中 - this.plugin.settings.backgrounds.push(newBg); - void this.plugin.saveSettings(); - - // 这里仅需刷新背景列表和时间规则列表 - this.displayBackgrounds(); - this.displayTimeRules(); - }); - - modal.open(); - } - - // 显示编辑背景的模态窗口 - private showEditBackgroundModal(bg: BackgroundItem, index: number): void { - const modal = new BackgroundModal(this.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.displayTimeRules(); - }); - - // 先打开模态窗口,然后预填充现有值 - modal.open(); - - // 等待模态窗口完全加载后再设置值 - setTimeout(() => { - if (modal.nameInput && modal.valueInput) { - modal.nameInput.value = bg.name; - modal.valueInput.value = bg.value; - } - }, 0); - } - - private showAddFolderModal(): void { - const modal = new ImageFolderSuggestModal(this.app, (folderPath: string) => { - if (!folderPath.trim()) { - new Notice(t("notice_valid_folder_path_required")); - return; - } - - // 检查文件夹是否存在 - const folder = this.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) => { - console.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.displayTimeRules(); - new Notice( - t("restore_default_bg_success", { - count: addedCount.toString(), - }) - ); - } else { - new Notice(t("restore_default_bg_no_new")); - } - } - - // 在指定的容器元素中渲染所有背景项 - displayBackgrounds(): void { - const container = this.bgListContainer; - container.empty(); - - // 初始化背景拖拽排序 - this.backgroundDragSort = new DragSort({ - 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.displayTimeRules(); - }, - }); - 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.displayTimeRules(); - }; - - // 启用拖拽功能 - this.backgroundDragSort?.enableDragForElement(bgEl, bg); - }); - } - - // 设置预览元素的背景样式 * 使用 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: - console.warn(`DTB: 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.app.vault.getFolderByPath(folderPath); - if (folder) { - // 只获取该文件夹下的直接子文件(不递归) - folderFiles = this.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) { - console.error("DTB: Error scanning folder:", error); - new Notice(t("folder_scan_error", { error: 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: Date.now().toString() + "-" + addedCount, // 确保ID唯一 - 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.displayTimeRules(); - new Notice(t("folder_scan_success", { count: addedCount.toString() })); - } else { - new Notice(t("folder_no_new_images")); - } - } - - // ============================================================================ - // 壁纸 API 管理 - // ============================================================================ - - /* - * 显示壁纸 API 管理设置 - */ - displayWallpaperApiSettings(): void { - const containerEl = this.wallpaperApiSettingsEl; - 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.displayWallpaperApiSettings(); - }); - }); - - // 添加 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() { - const container = this.apiListContainer; - container.empty(); - - // 初始化 API 拖拽排序 - this.apiDragSort = new DragSort({ - 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) { - console.warn(`DTB: 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) { - console.error(`DTB: 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.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.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.displayBackgrounds(); - this.displayTimeRules(); - - 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.message })); - console.error("DTB: Error fetching wallpaper:", error); - } - } - - /** - * 清理工作 - */ cleanup(): void { - // 使用组件ID清理该组件的所有订阅 - apiManager.stateManager.cleanupByComponent(this.componentId); + this.basicSection?.cleanup(); + this.modeSection?.cleanup(); + this.bgSection?.cleanup(); + this.apiSection?.cleanup(); - // 清理拖拽排序实例 - this.backgroundDragSort?.disableAllDrag(); - this.timeRuleDragSort?.disableAllDrag(); - this.apiDragSort?.disableAllDrag(); - } - - // ============================================================================ - // Utils - // ============================================================================ - - private genComponentId() { - return `settings-tab-${crypto.randomUUID()}`; + this.basicSection = undefined; + this.modeSection = undefined; + this.bgSection = undefined; + this.apiSection = undefined; } } diff --git a/src/settings/settings-view.ts b/src/settings/settings-view.ts index 1a20914..c155791 100644 --- a/src/settings/settings-view.ts +++ b/src/settings/settings-view.ts @@ -47,16 +47,16 @@ export class DTBSettingsView extends ItemView { } async onClose(): Promise { - // 清理设置标签页的订阅 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(); } } diff --git a/src/utils/drag-sort.ts b/src/utils/drag-sort.ts index 6995cfe..90844ea 100644 --- a/src/utils/drag-sort.ts +++ b/src/utils/drag-sort.ts @@ -3,6 +3,8 @@ * 支持对任意列表进行拖拽排序操作 */ +import { logger } from "../core/logger"; + /** * 拖拽排序配置接口 */ @@ -80,11 +82,10 @@ export class DragSort { 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 { 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; } diff --git a/src/utils/utils.ts b/src/utils/utils.ts index f81ee97..651f610 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -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; +} diff --git a/src/wallpaper-apis/core/api-manager.ts b/src/wallpaper-apis/core/api-manager.ts index 0a976ef..ff24ba7 100644 --- a/src/wallpaper-apis/core/api-manager.ts +++ b/src/wallpaper-apis/core/api-manager.ts @@ -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"); } } diff --git a/src/wallpaper-apis/core/api-registry.ts b/src/wallpaper-apis/core/api-registry.ts index 5d7f9a9..78cfbe2 100644 --- a/src/wallpaper-apis/core/api-registry.ts +++ b/src/wallpaper-apis/core/api-registry.ts @@ -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); diff --git a/src/wallpaper-apis/core/api-state-manager.ts b/src/wallpaper-apis/core/api-state-manager.ts index 9a5f1a1..f4eb811 100644 --- a/src/wallpaper-apis/core/api-state-manager.ts +++ b/src/wallpaper-apis/core/api-state-manager.ts @@ -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); diff --git a/src/wallpaper-apis/core/base-api.ts b/src/wallpaper-apis/core/base-api.ts index dda7281..e66f680 100644 --- a/src/wallpaper-apis/core/base-api.ts +++ b/src/wallpaper-apis/core/base-api.ts @@ -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; // 启用插件时必须调用 - abstract deinit(): Promise; // 禁用插件时必须调用 - abstract updateImageCache(): Promise; // 更新图片缓存数据 wallpaperImageCache + + /** + * 禁用时清理(默认实现,子类可覆写) + */ + deinit(): Promise { + if (!this.initialized) return Promise.resolve(true); + this.wallpaperImageCache = []; + this.curDataIndex = 0; + this.currentPage = 1; + this.initialized = false; + return Promise.resolve(true); + } + + /** + * 更新图片缓存(模板方法) + * 标准分页提供者只需覆写 fetchPage(),无需覆写此方法 + * 非标准分页提供者(如 qihoo360、custom)可直接覆写 + */ + async updateImageCache(): Promise { + 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 { + 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; } diff --git a/src/wallpaper-apis/providers/custom.ts b/src/wallpaper-apis/providers/custom.ts index 5619157..985d16e 100644 --- a/src/wallpaper-apis/providers/custom.ts +++ b/src/wallpaper-apis/providers/custom.ts @@ -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 { - 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 { @@ -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"); } } diff --git a/src/wallpaper-apis/providers/pexels.ts b/src/wallpaper-apis/providers/pexels.ts index 9b71ecb..216847a 100644 --- a/src/wallpaper-apis/providers/pexels.ts +++ b/src/wallpaper-apis/providers/pexels.ts @@ -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 { - 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 { - // 如果已经到最后一页了,跳到第一页 - 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 { + 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 { 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): WallpaperImage { const src = (photo.src as Record) ?? {}; diff --git a/src/wallpaper-apis/providers/pixabay.ts b/src/wallpaper-apis/providers/pixabay.ts index 2404818..1fde7e2 100644 --- a/src/wallpaper-apis/providers/pixabay.ts +++ b/src/wallpaper-apis/providers/pixabay.ts @@ -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 { - 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 { - // 如果已经到最后一页了,跳到第一页 - 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 { + protected async fetchPage(page: number): Promise { 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): WallpaperImage { const tagsStr = this.safeString(image.tags); diff --git a/src/wallpaper-apis/providers/qihoo360.ts b/src/wallpaper-apis/providers/qihoo360.ts index 09eed3b..32aaeff 100644 --- a/src/wallpaper-apis/providers/qihoo360.ts +++ b/src/wallpaper-apis/providers/qihoo360.ts @@ -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 { - this.initialized = false; this.categoriesCache = []; this.hotSearchCache = []; - return Promise.resolve(true); + return super.deinit(); // 重置 cache, curDataIndex, currentPage, initialized } async updateImageCache(): Promise { @@ -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 { 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, diff --git a/src/wallpaper-apis/providers/unsplash.ts b/src/wallpaper-apis/providers/unsplash.ts index 8ec067f..06de083 100644 --- a/src/wallpaper-apis/providers/unsplash.ts +++ b/src/wallpaper-apis/providers/unsplash.ts @@ -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 { - 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 { - // 如果有搜索查询,使用搜索接口 - 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 { + 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 { 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): WallpaperImage { const user = (photo.user as Record) ?? {}; diff --git a/src/wallpaper-apis/providers/wallhaven.ts b/src/wallpaper-apis/providers/wallhaven.ts index cf95a31..70ffbff 100644 --- a/src/wallpaper-apis/providers/wallhaven.ts +++ b/src/wallpaper-apis/providers/wallhaven.ts @@ -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 { - 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 { - // 如果已经到最后一页了,跳到第一页 - 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 { + protected async fetchPage(page: number): Promise { 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) => 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): WallpaperImage { return { diff --git a/tsconfig.json b/tsconfig.json index f91e520..3a392c5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -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",