mirror of
https://github.com/sean2077/obsidian-dynamic-theme-background.git
synced 2026-07-22 06:44:57 +00:00
feat: decompose plugin and settings into service-oriented architecture
Extract 6 core services from the monolithic plugin.ts (819→284 lines): - EventBus: unified typed event system replacing 3 ad-hoc notification mechanisms - TimeRuleScheduler: pure time-rule matching and scheduling logic - StyleManager: CSS variable injection, image analysis with size caching - BackgroundManager: lifecycle, timer management, and background selection - BackgroundPersistence: immutable vault file I/O (no longer mutates BackgroundItem) - Logger: centralized logging with consistent prefix Decompose settings-tab.ts (1417→147 lines) into 4 focused sections: - BasicSettingsSection, ModeSettingsSection, BgManagementSection, ApiSettingsSection Additional improvements: - Enable TypeScript strict mode - Pin all devDependency versions for reproducible builds - Template method pattern for provider updateImageCache/deinit/fetchPage - Move safeString() to BaseWallpaperApi, consolidate ID generation - Fix 26 bugs: deleteApi missing deinit, getImages infinite loop, settings-view memory leak, manual mode bounds check, statusBar DOM leak, test API ID collision, rule lookup by .name→.id, DragSort cleanup, perPage ordering, and more
This commit is contained in:
parent
198e3d2e56
commit
62d855ffbd
33 changed files with 2702 additions and 2363 deletions
32
package.json
32
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"
|
||||
|
|
|
|||
15
src/constants.ts
Normal file
15
src/constants.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
/**
|
||||
* 全局常量定义
|
||||
*/
|
||||
|
||||
// 定时器间隔
|
||||
export const MIN_DELAY_MS = 1000;
|
||||
export const MS_PER_MINUTE = 60_000;
|
||||
export const FALLBACK_CHECK_MS = 24 * 60 * 60 * 1000; // 24 小时
|
||||
|
||||
// 图片比例分析阈值
|
||||
export const RATIO_CLOSE_THRESHOLD = 0.1; // 比例差异 < 10% 视为相近
|
||||
export const RATIO_LARGE_DIFF_THRESHOLD = 0.5; // 比例差异 > 50% 视为差异很大
|
||||
|
||||
// 壁纸缓存
|
||||
export const MAX_WALLPAPER_CACHE_SIZE = 100;
|
||||
266
src/core/background-manager.ts
Normal file
266
src/core/background-manager.ts
Normal file
|
|
@ -0,0 +1,266 @@
|
|||
/**
|
||||
* 背景生命周期管理器
|
||||
* 负责背景切换调度、模式选择和定时器管理
|
||||
*/
|
||||
|
||||
import { Notice, type Plugin } from "obsidian";
|
||||
|
||||
import { FALLBACK_CHECK_MS, MIN_DELAY_MS, MS_PER_MINUTE } from "../constants";
|
||||
import { t } from "../i18n";
|
||||
import type { BackgroundItem, DTBSettings } from "../types";
|
||||
import { apiManager } from "../wallpaper-apis";
|
||||
import type { EventBus } from "./event-bus";
|
||||
import { logger } from "./logger";
|
||||
import type { StyleManager } from "./style-manager";
|
||||
import type { TimeRuleScheduler } from "./time-rule-scheduler";
|
||||
|
||||
export class BackgroundManager {
|
||||
private scheduler: TimeRuleScheduler;
|
||||
private styleManager: StyleManager;
|
||||
private events: EventBus;
|
||||
private plugin: Plugin;
|
||||
|
||||
// 状态
|
||||
background: BackgroundItem | null = null;
|
||||
private intervalId: number | null = null;
|
||||
private timeoutId: number | null = null;
|
||||
private lastActiveRuleId: string | null = null;
|
||||
private isUpdating = false;
|
||||
private onSettingsMutated: (() => void) | null = null;
|
||||
|
||||
constructor(
|
||||
scheduler: TimeRuleScheduler,
|
||||
styleManager: StyleManager,
|
||||
events: EventBus,
|
||||
plugin: Plugin,
|
||||
onSettingsMutated?: () => void
|
||||
) {
|
||||
this.scheduler = scheduler;
|
||||
this.styleManager = styleManager;
|
||||
this.events = events;
|
||||
this.plugin = plugin;
|
||||
this.onSettingsMutated = onSettingsMutated ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止背景管理器
|
||||
*/
|
||||
stop(): void {
|
||||
if (this.intervalId) {
|
||||
clearInterval(this.intervalId);
|
||||
this.intervalId = null;
|
||||
}
|
||||
if (this.timeoutId) {
|
||||
clearTimeout(this.timeoutId);
|
||||
this.timeoutId = null;
|
||||
}
|
||||
document.body.classList.remove("dtb-enabled");
|
||||
logger.debug("Background manager stopped");
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动背景管理器
|
||||
*/
|
||||
start(settings: DTBSettings): void {
|
||||
this.stop();
|
||||
|
||||
if (!document.body.classList.contains("dtb-enabled")) {
|
||||
document.body.classList.add("dtb-enabled");
|
||||
}
|
||||
|
||||
// 立即执行一次更新
|
||||
void this.update(settings, true);
|
||||
|
||||
if (settings.mode === "time-based") {
|
||||
this.startTimeBasedMode(settings);
|
||||
} else if (settings.mode === "interval") {
|
||||
const intervalMs = settings.intervalMinutes * MS_PER_MINUTE;
|
||||
this.intervalId = this.plugin.registerInterval(
|
||||
window.setInterval(() => {
|
||||
void this.update(settings, false);
|
||||
}, intervalMs)
|
||||
);
|
||||
|
||||
logger.debug("Background manager started (interval mode)", {
|
||||
mode: settings.mode,
|
||||
interval: intervalMs / 1000 + "s",
|
||||
});
|
||||
} else {
|
||||
logger.debug("Background manager started (manual mode)", {
|
||||
mode: settings.mode,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 时段规则模式调度
|
||||
*/
|
||||
private startTimeBasedMode(settings: DTBSettings): void {
|
||||
const scheduleNext = () => {
|
||||
const nextRuleChange = this.scheduler.getNextChangeTime();
|
||||
if (nextRuleChange) {
|
||||
const delay = nextRuleChange - Date.now();
|
||||
const actualDelay = Math.max(delay, MIN_DELAY_MS);
|
||||
|
||||
this.timeoutId = window.setTimeout(() => {
|
||||
void this.update(settings, false);
|
||||
// 只在活跃规则变化时才刷新 UI
|
||||
const currentRule = this.scheduler.getCurrentRule();
|
||||
const currentRuleId = currentRule?.id ?? null;
|
||||
if (currentRuleId !== this.lastActiveRuleId) {
|
||||
this.lastActiveRuleId = currentRuleId;
|
||||
this.events.emit("time-rules:changed", {});
|
||||
}
|
||||
scheduleNext();
|
||||
}, actualDelay);
|
||||
|
||||
logger.debug("Next background change scheduled", {
|
||||
mode: settings.mode,
|
||||
delay: Math.round(actualDelay / 1000) + "s",
|
||||
nextTime: new Date(nextRuleChange).toLocaleTimeString(),
|
||||
});
|
||||
} else {
|
||||
this.timeoutId = window.setTimeout(
|
||||
() => {
|
||||
void this.update(settings, false);
|
||||
scheduleNext();
|
||||
},
|
||||
FALLBACK_CHECK_MS
|
||||
);
|
||||
|
||||
logger.debug("No next rule found, checking again in 24 hours");
|
||||
}
|
||||
};
|
||||
|
||||
scheduleNext();
|
||||
}
|
||||
|
||||
/**
|
||||
* 按设定规则更新背景
|
||||
*/
|
||||
async update(settings: DTBSettings, forceUpdate = true): Promise<void> {
|
||||
if (!settings.enabled) return;
|
||||
|
||||
// 防止并发更新
|
||||
if (this.isUpdating) return;
|
||||
this.isUpdating = true;
|
||||
|
||||
try {
|
||||
let needsUpdate = false;
|
||||
switch (settings.mode) {
|
||||
case "time-based": {
|
||||
const rule = this.scheduler.getCurrentRule();
|
||||
if (rule) {
|
||||
needsUpdate = this.background?.id !== rule.backgroundId;
|
||||
if (needsUpdate) {
|
||||
this.background =
|
||||
settings.backgrounds.find((bg) => bg.id === rule.backgroundId) ?? null;
|
||||
}
|
||||
} else {
|
||||
this.background = null;
|
||||
needsUpdate = true;
|
||||
}
|
||||
logger.debug("TimeRule mode - current time rule", rule, needsUpdate);
|
||||
break;
|
||||
}
|
||||
case "interval": {
|
||||
const randomBg = await this.fetchRandomWallpaper(settings);
|
||||
if (randomBg) {
|
||||
this.background = randomBg;
|
||||
needsUpdate = true;
|
||||
} else if (settings.backgrounds.length > 0) {
|
||||
settings.currentIndex =
|
||||
(settings.currentIndex + 1) % settings.backgrounds.length;
|
||||
this.background = settings.backgrounds[settings.currentIndex];
|
||||
this.onSettingsMutated?.();
|
||||
needsUpdate = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
this.background = settings.backgrounds[settings.currentIndex] ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
if (forceUpdate || needsUpdate) {
|
||||
this.styleManager.applyBackground(this.background, settings);
|
||||
this.events.emit("backgrounds:changed", {});
|
||||
}
|
||||
} finally {
|
||||
this.isUpdating = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用随机壁纸
|
||||
*/
|
||||
async applyRandom(settings: DTBSettings): Promise<void> {
|
||||
const bg = await this.fetchRandomWallpaper(settings);
|
||||
|
||||
if (bg) {
|
||||
this.background = bg;
|
||||
} else if (settings.backgrounds.length > 0) {
|
||||
settings.currentIndex = (settings.currentIndex + 1) % settings.backgrounds.length;
|
||||
this.background = settings.backgrounds[settings.currentIndex];
|
||||
this.onSettingsMutated?.();
|
||||
}
|
||||
|
||||
this.styleManager.applyBackground(this.background, settings);
|
||||
this.events.emit("backgrounds:changed", {});
|
||||
}
|
||||
|
||||
/**
|
||||
* 从壁纸 API 获取随机图片
|
||||
*/
|
||||
private async fetchRandomWallpaper(settings: DTBSettings): Promise<BackgroundItem | null> {
|
||||
if (!settings.enableRandomWallpaper) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const enabledApis = apiManager.getEnabledApis();
|
||||
if (enabledApis.length === 0) {
|
||||
logger.warn("No enabled APIs found");
|
||||
return null;
|
||||
}
|
||||
|
||||
const selectedApi = enabledApis[Math.floor(Math.random() * enabledApis.length)];
|
||||
|
||||
try {
|
||||
const loadingNotice = new Notice(
|
||||
t("notice_api_fetching", { apiName: selectedApi.getName() }),
|
||||
0
|
||||
);
|
||||
|
||||
const wallpaperImages = await apiManager.getRandomWallpapers(selectedApi.getId());
|
||||
loadingNotice.hide();
|
||||
|
||||
if (!wallpaperImages || wallpaperImages.length === 0) {
|
||||
logger.warn(`No images returned from API: ${selectedApi.getName()}`);
|
||||
return null;
|
||||
}
|
||||
const randomImage = wallpaperImages[Math.floor(Math.random() * wallpaperImages.length)];
|
||||
if (randomImage?.url) {
|
||||
new Notice(t("notice_api_success_applied", { apiName: selectedApi.getName() }));
|
||||
return {
|
||||
id: selectedApi.generateBackgroundId(),
|
||||
name: selectedApi.generateBackgroundName(),
|
||||
type: "image",
|
||||
value: randomImage.url,
|
||||
};
|
||||
} else {
|
||||
new Notice(t("notice_api_failed_fetch", { apiName: selectedApi.getName() }));
|
||||
logger.warn(`No wallpaper image returned from API: ${selectedApi.getName()}`);
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
new Notice(
|
||||
t("notice_api_error_fetch", {
|
||||
apiName: selectedApi.getName(),
|
||||
error: (error as Error).message,
|
||||
})
|
||||
);
|
||||
logger.error("Error fetching random wallpaper:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
88
src/core/background-persistence.ts
Normal file
88
src/core/background-persistence.ts
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
/**
|
||||
* 背景持久化服务
|
||||
* 负责远程图片下载保存到 vault
|
||||
*/
|
||||
|
||||
import { type App, Notice, requestUrl } from "obsidian";
|
||||
|
||||
import { t } from "../i18n";
|
||||
import { confirm } from "../modals";
|
||||
import type { BackgroundItem } from "../types";
|
||||
import { logger } from "./logger";
|
||||
|
||||
export interface SaveResult {
|
||||
success: true;
|
||||
/** 更新后的 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<SaveResult | SaveError> {
|
||||
if (!folderPath) {
|
||||
return { success: false, error: "no_folder_path" };
|
||||
}
|
||||
|
||||
// 规范化文件名
|
||||
const imageName = bg.name.replace(/[\\/:*?"<>|]/g, "_") + ".jpg";
|
||||
const localPath = `${folderPath}/${imageName}`;
|
||||
|
||||
// 判断路径是否存在
|
||||
const file = this.app.vault.getFileByPath(localPath);
|
||||
if (file) {
|
||||
const overwrite = await confirm(
|
||||
this.app,
|
||||
t("notice_save_background_overwrite_existing_file", { filePath: localPath })
|
||||
);
|
||||
if (!overwrite) {
|
||||
return { success: false, error: "cancelled" };
|
||||
}
|
||||
}
|
||||
|
||||
// 下载远程图片
|
||||
try {
|
||||
const response = await requestUrl({ url: bg.value });
|
||||
if (response.status < 200 || response.status >= 300) {
|
||||
new Notice(t("notice_save_background_failed"), response.status);
|
||||
return { success: false, error: `HTTP ${response.status}` };
|
||||
}
|
||||
const arrayBuffer = response.arrayBuffer;
|
||||
|
||||
// 覆盖旧文件
|
||||
if (file) {
|
||||
await this.app.fileManager.trashFile(file);
|
||||
}
|
||||
await this.app.vault.createBinary(localPath, arrayBuffer);
|
||||
|
||||
// 创建新的 BackgroundItem(不修改原对象)
|
||||
const updatedBg: BackgroundItem = {
|
||||
...bg,
|
||||
remoteUrl: bg.value,
|
||||
value: localPath,
|
||||
};
|
||||
|
||||
new Notice(
|
||||
t("notice_save_background_converted", {
|
||||
oldPath: bg.value,
|
||||
newPath: localPath,
|
||||
}),
|
||||
5000
|
||||
);
|
||||
|
||||
return { success: true, updatedBg };
|
||||
} catch (error) {
|
||||
logger.error("Error saving remote image:", error);
|
||||
return { success: false, error: String(error) };
|
||||
}
|
||||
}
|
||||
}
|
||||
92
src/core/event-bus.ts
Normal file
92
src/core/event-bus.ts
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
/**
|
||||
* 统一的类型安全事件总线
|
||||
* 替代原有的多种通知机制(ApiStateManager 的观察者模式、插件直接方法调用、workspace 事件)
|
||||
*/
|
||||
|
||||
import type { BackgroundItem, DTBSettings } from "../types";
|
||||
|
||||
/**
|
||||
* API 状态信息
|
||||
*/
|
||||
export interface ApiStateInfo {
|
||||
configEnabled: boolean;
|
||||
instanceEnabled: boolean;
|
||||
isLoading: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 事件映射:事件名 → 载荷类型
|
||||
*/
|
||||
export interface DTBEventMap {
|
||||
"background:changed": { background: BackgroundItem | null };
|
||||
"settings:changed": { key: keyof DTBSettings; value: unknown };
|
||||
"api:state-changed": { apiId: string; state: ApiStateInfo };
|
||||
"time-rules:changed": Record<string, never>;
|
||||
"backgrounds:changed": Record<string, never>;
|
||||
"apis:changed": Record<string, never>;
|
||||
"css:updated": Record<string, never>;
|
||||
}
|
||||
|
||||
export type DTBEventKey = keyof DTBEventMap;
|
||||
|
||||
type Listener<T> = (payload: T) => void;
|
||||
|
||||
/**
|
||||
* 类型安全的事件总线
|
||||
*/
|
||||
export class EventBus {
|
||||
private listeners = new Map<string, Set<Listener<unknown>>>();
|
||||
|
||||
/**
|
||||
* 订阅事件,返回取消订阅函数
|
||||
*/
|
||||
on<K extends DTBEventKey>(event: K, cb: Listener<DTBEventMap[K]>): () => void {
|
||||
if (!this.listeners.has(event)) {
|
||||
this.listeners.set(event, new Set());
|
||||
}
|
||||
const set = this.listeners.get(event)!;
|
||||
set.add(cb as Listener<unknown>);
|
||||
|
||||
return () => {
|
||||
set.delete(cb as Listener<unknown>);
|
||||
if (set.size === 0) {
|
||||
this.listeners.delete(event);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送事件(同步通知所有监听器)
|
||||
*/
|
||||
emit<K extends DTBEventKey>(event: K, payload: DTBEventMap[K]): void {
|
||||
const set = this.listeners.get(event);
|
||||
if (!set) return;
|
||||
for (const cb of [...set]) {
|
||||
try {
|
||||
cb(payload);
|
||||
} catch (error) {
|
||||
console.error(`DTB EventBus: error in listener for "${event}":`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除指定事件的指定监听器
|
||||
*/
|
||||
off<K extends DTBEventKey>(event: K, cb: Listener<DTBEventMap[K]>): void {
|
||||
const set = this.listeners.get(event);
|
||||
if (!set) return;
|
||||
set.delete(cb as Listener<unknown>);
|
||||
if (set.size === 0) {
|
||||
this.listeners.delete(event);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除所有监听器
|
||||
*/
|
||||
removeAllListeners(): void {
|
||||
this.listeners.clear();
|
||||
}
|
||||
}
|
||||
8
src/core/index.ts
Normal file
8
src/core/index.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
export { BackgroundManager } from "./background-manager";
|
||||
export { BackgroundPersistence } from "./background-persistence";
|
||||
export type { SaveError, SaveResult } from "./background-persistence";
|
||||
export { EventBus } from "./event-bus";
|
||||
export type { ApiStateInfo, DTBEventKey, DTBEventMap } from "./event-bus";
|
||||
export { logger } from "./logger";
|
||||
export { StyleManager } from "./style-manager";
|
||||
export { TimeRuleScheduler } from "./time-rule-scheduler";
|
||||
12
src/core/logger.ts
Normal file
12
src/core/logger.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
/**
|
||||
* 统一日志工具
|
||||
* 替代分散的 console.debug/warn/error 调用,统一 "DTB" 前缀
|
||||
*/
|
||||
|
||||
const PREFIX = "DTB";
|
||||
|
||||
export const logger = {
|
||||
debug: (msg: string, ...args: unknown[]) => console.debug(`${PREFIX}: ${msg}`, ...args),
|
||||
warn: (msg: string, ...args: unknown[]) => console.warn(`${PREFIX}: ${msg}`, ...args),
|
||||
error: (msg: string, ...args: unknown[]) => console.error(`${PREFIX}: ${msg}`, ...args),
|
||||
};
|
||||
200
src/core/style-manager.ts
Normal file
200
src/core/style-manager.ts
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
/**
|
||||
* 样式管理器
|
||||
* 负责 CSS 变量注入、背景 URL 解析和图片尺寸分析
|
||||
*/
|
||||
|
||||
import type { App } from "obsidian";
|
||||
|
||||
import { RATIO_CLOSE_THRESHOLD, RATIO_LARGE_DIFF_THRESHOLD } from "../constants";
|
||||
import type { BackgroundItem, DTBSettings } from "../types";
|
||||
import { hexToRgba } from "../utils";
|
||||
import { logger } from "./logger";
|
||||
|
||||
export class StyleManager {
|
||||
private app: App;
|
||||
private imageSizeCache = new Map<string, string>();
|
||||
|
||||
constructor(app: App) {
|
||||
this.app = app;
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用背景样式(CSS 变量注入)
|
||||
*/
|
||||
applyBackground(bg: BackgroundItem | null, settings: DTBSettings, bgSize?: string): void {
|
||||
if (!settings.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!bg) {
|
||||
// 没有激活背景时,仍然更新遮罩变量
|
||||
const bgColorLight = hexToRgba(settings.bgColorLight, settings.bgColorOpacityLight);
|
||||
const bgColorDark = hexToRgba(settings.bgColorDark, settings.bgColorOpacityDark);
|
||||
document.documentElement.setCssProps({
|
||||
"--dtb-bg-image": "none",
|
||||
"--dtb-bg-color-light": bgColorLight,
|
||||
"--dtb-bg-color-dark": bgColorDark,
|
||||
});
|
||||
} else {
|
||||
const bgCssValue = bg.type === "image" ? this.getBgURL(bg) : bg.value;
|
||||
// 优先级: 传入的自定义值 > 背景单独的设置 > 全局默认设置
|
||||
const blurDepth = bg.blurDepth ?? settings.blurDepth;
|
||||
const brightness4Bg = bg.brightness4Bg ?? settings.brightness4Bg;
|
||||
const saturate4Bg = bg.saturate4Bg ?? settings.saturate4Bg;
|
||||
// 遮罩颜色与透明度
|
||||
const lightColor = bg.bgColorLight ?? settings.bgColorLight;
|
||||
const lightOpacity = bg.bgColorOpacityLight ?? settings.bgColorOpacityLight;
|
||||
const darkColor = bg.bgColorDark ?? settings.bgColorDark;
|
||||
const darkOpacity = bg.bgColorOpacityDark ?? settings.bgColorOpacityDark;
|
||||
const bgColorLight = hexToRgba(lightColor, lightOpacity);
|
||||
const bgColorDark = hexToRgba(darkColor, darkOpacity);
|
||||
bgSize = bgSize ?? bg.bgSize ?? settings.bgSize ?? "intelligent";
|
||||
// intelligent 模式下动态选择
|
||||
if (bgSize === "intelligent") {
|
||||
if (bg.type === "image") {
|
||||
bgSize = this.getOptimalBackgroundSize(bg.value);
|
||||
} else {
|
||||
bgSize = "auto";
|
||||
}
|
||||
}
|
||||
document.documentElement.setCssProps({
|
||||
"--dtb-bg-image": bgCssValue,
|
||||
"--dtb-blur-depth": `${blurDepth}px`,
|
||||
"--dtb-brightness": `${brightness4Bg}`,
|
||||
"--dtb-saturate": `${saturate4Bg}`,
|
||||
"--dtb-bg-color-light": bgColorLight,
|
||||
"--dtb-bg-color-dark": bgColorDark,
|
||||
"--dtb-bg-size": bgSize,
|
||||
});
|
||||
}
|
||||
|
||||
// 通知 css-change
|
||||
this.app.workspace.trigger("css-change", { source: "dtb" });
|
||||
}
|
||||
|
||||
/**
|
||||
* 将图片路径转换为可用的 CSS URL
|
||||
* 注意:如果本地路径无效且有 remoteUrl 备份,返回远端 URL 并标记需要恢复
|
||||
*/
|
||||
getBgURL(bg: BackgroundItem): string {
|
||||
const imagePath = bg.value;
|
||||
if (this.isRemoteImage(imagePath)) {
|
||||
return `url("${imagePath}")`;
|
||||
}
|
||||
// 本地图片路径
|
||||
const file = this.app.vault.getFileByPath(imagePath);
|
||||
if (file) {
|
||||
const p = this.app.vault.getResourcePath(file);
|
||||
if (p) {
|
||||
return `url("${p}")`;
|
||||
} else {
|
||||
logger.warn(`Resource path for ${imagePath} is empty`);
|
||||
}
|
||||
} else {
|
||||
logger.warn(`Image ${imagePath} not found or inaccessible`);
|
||||
}
|
||||
|
||||
// 回退到 remoteUrl 备份(不修改 bg 对象,仅使用远端 URL 渲染)
|
||||
if (bg.remoteUrl) {
|
||||
logger.warn(`Local path invalid, falling back to remote URL for "${bg.name}"`);
|
||||
return `url("${bg.remoteUrl}")`;
|
||||
}
|
||||
|
||||
return "none";
|
||||
}
|
||||
|
||||
isRemoteImage(imagePath: string): boolean {
|
||||
return imagePath.startsWith("http://") || imagePath.startsWith("https://");
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据图片和屏幕比例动态选择最佳 background-size
|
||||
*/
|
||||
private getOptimalBackgroundSize(imagePath: string): string {
|
||||
if (this.isRemoteImage(imagePath)) {
|
||||
return "contain";
|
||||
}
|
||||
|
||||
// 检查缓存
|
||||
const cached = this.imageSizeCache.get(imagePath);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
try {
|
||||
const screenRatio = window.innerWidth / window.innerHeight;
|
||||
const file = this.app.vault.getFileByPath(imagePath);
|
||||
if (!file) return "contain";
|
||||
|
||||
const resourcePath = this.app.vault.getResourcePath(file);
|
||||
if (!resourcePath) return "contain";
|
||||
|
||||
// 异步分析图片尺寸
|
||||
void this.loadImageAndAnalyze(resourcePath, screenRatio, imagePath);
|
||||
|
||||
return "contain"; // 默认返回 contain,异步更新后会重新渲染
|
||||
} catch (error) {
|
||||
logger.warn("Error determining optimal background size:", error);
|
||||
return "contain";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步加载图片并分析尺寸,结果缓存
|
||||
* 返回计算出的最佳尺寸(通过回调通知)
|
||||
*/
|
||||
private async loadImageAndAnalyze(
|
||||
resourcePath: string,
|
||||
screenRatio: number,
|
||||
imagePath: string
|
||||
): Promise<string> {
|
||||
try {
|
||||
const img = new Image();
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
img.onload = () => resolve();
|
||||
img.onerror = () => reject(new Error("Failed to load image"));
|
||||
img.src = resourcePath;
|
||||
});
|
||||
|
||||
const imageRatio = img.naturalWidth / img.naturalHeight;
|
||||
const ratioDifference = Math.abs(imageRatio - screenRatio) / screenRatio;
|
||||
|
||||
let optimalSize: string;
|
||||
|
||||
if (ratioDifference < RATIO_CLOSE_THRESHOLD) {
|
||||
optimalSize = "cover";
|
||||
} else if (imageRatio > screenRatio) {
|
||||
optimalSize = "contain";
|
||||
} else {
|
||||
optimalSize =
|
||||
ratioDifference > RATIO_LARGE_DIFF_THRESHOLD ? "contain" : "cover";
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`Image analysis - Screen: ${screenRatio.toFixed(2)}, Image: ${imageRatio.toFixed(2)}, Size: ${optimalSize}`
|
||||
);
|
||||
|
||||
// 缓存结果
|
||||
this.imageSizeCache.set(imagePath, optimalSize);
|
||||
|
||||
// 限制缓存大小
|
||||
if (this.imageSizeCache.size > 50) {
|
||||
const firstKey = this.imageSizeCache.keys().next().value;
|
||||
if (firstKey) this.imageSizeCache.delete(firstKey);
|
||||
}
|
||||
|
||||
return optimalSize;
|
||||
} catch (error) {
|
||||
logger.warn("Error loading image for size calculation:", error);
|
||||
return "contain";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除图片尺寸缓存(窗口 resize 时调用)
|
||||
*/
|
||||
clearImageSizeCache(): void {
|
||||
this.imageSizeCache.clear();
|
||||
}
|
||||
}
|
||||
131
src/core/time-rule-scheduler.ts
Normal file
131
src/core/time-rule-scheduler.ts
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
/**
|
||||
* 时段规则调度器
|
||||
* 纯逻辑服务:时间规则匹配和下一次切换时间计算
|
||||
*/
|
||||
|
||||
import type { TimeRule } from "../types";
|
||||
|
||||
export class TimeRuleScheduler {
|
||||
private rules: TimeRule[] = [];
|
||||
|
||||
constructor(rules: TimeRule[]) {
|
||||
this.rules = rules;
|
||||
}
|
||||
|
||||
updateRules(rules: TimeRule[]): void {
|
||||
this.rules = rules;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前时段规则
|
||||
* 按时间顺序排序规则,找到第一个匹配当前时间的规则
|
||||
*/
|
||||
getCurrentRule(now?: Date): TimeRule | null {
|
||||
now = now ?? new Date();
|
||||
const currentTimeMinutes = now.getHours() * 60 + now.getMinutes();
|
||||
|
||||
// 按 startTime 排序,优先匹配靠前的规则
|
||||
const sortedRules = this.rules
|
||||
.filter((rule) => rule.enabled)
|
||||
.map((rule) => {
|
||||
const { startTime, endTime } = this.parseTimeRule(rule);
|
||||
return { rule, startTime, endTime };
|
||||
})
|
||||
.sort((a, b) => a.startTime - b.startTime);
|
||||
|
||||
// 遍历排序后的规则,找到第一个匹配的
|
||||
for (const { rule, startTime, endTime } of sortedRules) {
|
||||
// 跨天时段处理:如 20:00-06:00
|
||||
if (startTime > endTime) {
|
||||
if (currentTimeMinutes >= startTime || currentTimeMinutes < endTime) {
|
||||
return rule;
|
||||
}
|
||||
} else {
|
||||
if (currentTimeMinutes >= startTime && currentTimeMinutes < endTime) {
|
||||
return rule;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取下一个时段规则变化的时间点
|
||||
*/
|
||||
getNextChangeTime(now?: Date): number | null {
|
||||
const enabledRules = this.rules.filter((r) => r.enabled);
|
||||
if (enabledRules.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
now = now ?? new Date();
|
||||
const currentTimeMinutes = now.getHours() * 60 + now.getMinutes();
|
||||
|
||||
// 收集所有启用的时段规则的开始和结束时间点
|
||||
const timePoints: Array<{ time: number; isStart: boolean; rule: TimeRule }> = [];
|
||||
|
||||
for (const rule of this.rules) {
|
||||
if (!rule.enabled) continue;
|
||||
|
||||
const { startTime, endTime } = this.parseTimeRule(rule);
|
||||
|
||||
timePoints.push({ time: startTime, isStart: true, rule });
|
||||
timePoints.push({ time: endTime, isStart: false, rule });
|
||||
}
|
||||
|
||||
// 按时间排序
|
||||
timePoints.sort((a, b) => a.time - b.time);
|
||||
|
||||
// 查找下一个时间点
|
||||
let nextPoint = null;
|
||||
|
||||
// 首先查找今天剩余时间内的下一个时间点
|
||||
for (const point of timePoints) {
|
||||
if (point.time > currentTimeMinutes) {
|
||||
nextPoint = point;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果今天没有找到,取明天的第一个时间点
|
||||
if (!nextPoint && timePoints.length > 0) {
|
||||
nextPoint = timePoints[0];
|
||||
}
|
||||
|
||||
if (!nextPoint) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 计算具体的时间戳
|
||||
const targetDate = new Date(now);
|
||||
const targetHours = Math.floor(nextPoint.time / 60);
|
||||
const targetMinutes = nextPoint.time % 60;
|
||||
|
||||
targetDate.setHours(targetHours, targetMinutes, 0, 0);
|
||||
|
||||
// 如果目标时间已经过了,说明是明天的时间点
|
||||
if (targetDate.getTime() <= now.getTime()) {
|
||||
targetDate.setDate(targetDate.getDate() + 1);
|
||||
}
|
||||
|
||||
return targetDate.getTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析时间规则,返回标准化的分钟数
|
||||
*/
|
||||
parseTimeRule(rule: TimeRule): { startTime: number; endTime: number } {
|
||||
const [startHour, startMin] = rule.startTime.split(":").map(Number);
|
||||
const [endHour, endMin] = rule.endTime.split(":").map(Number);
|
||||
|
||||
// 防御性检查:格式异常时返回 0:00-0:00(等价于禁用)
|
||||
if (isNaN(startHour) || isNaN(startMin) || isNaN(endHour) || isNaN(endMin)) {
|
||||
return { startTime: 0, endTime: 0 };
|
||||
}
|
||||
|
||||
const startTime = startHour * 60 + startMin;
|
||||
const endTime = endHour * 60 + endMin;
|
||||
|
||||
return { startTime, endTime };
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import { App, Modal, Notice } from "obsidian";
|
||||
import { logger } from "../core/logger";
|
||||
import { generateId } from "../utils/utils";
|
||||
import { t } from "../i18n";
|
||||
import {
|
||||
apiManager,
|
||||
|
|
@ -20,23 +22,23 @@ export class WallpaperApiEditorModal extends Modal {
|
|||
onSubmit: (apiConfig: WallpaperApiConfig) => void;
|
||||
|
||||
// 基础配置输入元素
|
||||
nameInput: HTMLInputElement;
|
||||
descInput: HTMLInputElement; // 可选描述输入
|
||||
typeSelect: HTMLSelectElement;
|
||||
urlInput: HTMLInputElement;
|
||||
nameInput!: HTMLInputElement;
|
||||
descInput!: HTMLInputElement; // 可选描述输入
|
||||
typeSelect!: HTMLSelectElement;
|
||||
urlInput!: HTMLInputElement;
|
||||
// Headers配置
|
||||
headersContainer: HTMLDivElement;
|
||||
headersContainer!: HTMLDivElement;
|
||||
headerInputs: Array<{ key: HTMLInputElement; value: HTMLInputElement }> = [];
|
||||
|
||||
// 参数配置容器的引用
|
||||
paramsSectionContainer: HTMLElement;
|
||||
paramsSectionContainer!: HTMLElement;
|
||||
// 动态参数输入元素映射
|
||||
paramInputs: Map<string, HTMLElement> = new Map();
|
||||
// 额外参数输入
|
||||
extraParamsTextarea: HTMLTextAreaElement;
|
||||
extraParamsTextarea!: HTMLTextAreaElement;
|
||||
|
||||
// 自定义设置容器的引用
|
||||
customSettingsSectionContainer: HTMLElement;
|
||||
customSettingsSectionContainer!: HTMLElement;
|
||||
// 自定义设置输入元素
|
||||
customSettingsInputs: Map<string, HTMLElement> = new Map();
|
||||
|
||||
|
|
@ -400,7 +402,7 @@ export class WallpaperApiEditorModal extends Modal {
|
|||
try {
|
||||
currentValue = descriptor.fromApiValue(currentValue as ApiValueType);
|
||||
} catch (error) {
|
||||
console.warn(`Failed to convert value for ${descriptor.key}:`, error);
|
||||
logger.warn(`Failed to convert value for ${descriptor.key}:`, error);
|
||||
currentValue = descriptor.defaultValue;
|
||||
}
|
||||
}
|
||||
|
|
@ -620,7 +622,7 @@ export class WallpaperApiEditorModal extends Modal {
|
|||
});
|
||||
|
||||
return {
|
||||
id: this.apiConfig.id || `api-${Date.now()}`,
|
||||
id: this.apiConfig.id || generateId("api"),
|
||||
name: this.nameInput.value || t("api_modal_unnamed_api"),
|
||||
description: this.descInput.value || "",
|
||||
type: this.typeSelect.value as WallpaperApiType,
|
||||
|
|
@ -718,14 +720,15 @@ export class WallpaperApiEditorModal extends Modal {
|
|||
return;
|
||||
}
|
||||
|
||||
// 这里需临时创建个实例来测试
|
||||
await apiManager.createApi(config);
|
||||
// 如果能成功启用则测试通过
|
||||
await apiManager.enableApi(config.id);
|
||||
// 使用临时 ID 避免覆盖同 ID 的已有实例
|
||||
const testConfig = { ...config, id: generateId("api-test") };
|
||||
|
||||
await apiManager.createApi(testConfig);
|
||||
await apiManager.enableApi(testConfig.id);
|
||||
|
||||
new Notice(t("api_modal_test_successful"));
|
||||
|
||||
apiManager.deleteApi(config.id); // 测试后删除临时实例
|
||||
apiManager.deleteApi(testConfig.id);
|
||||
} catch (error) {
|
||||
new Notice(t("api_modal_test_failed", { error: (error as Error).message }));
|
||||
}
|
||||
|
|
|
|||
812
src/plugin.ts
812
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<string, DTBSettingTab> = new Map(); // 存储所有设置标签页的引用
|
||||
statusBar: HTMLElement | null = null;
|
||||
settingTabs: Map<string, DTBSettingTab> = new Map();
|
||||
|
||||
// ============================================================================
|
||||
// 主要接口方法
|
||||
|
|
@ -31,10 +33,13 @@ export default class DynamicThemeBackgroundPlugin extends Plugin {
|
|||
async onload() {
|
||||
await this.loadSettings();
|
||||
|
||||
// 左侧栏图标
|
||||
// this.addRibbonIcon("rainbow", "🌈 DTB", async (evt: MouseEvent) => {
|
||||
// await this.applyRandomWallpaper();
|
||||
// });
|
||||
// 初始化核心服务
|
||||
this.scheduler = new TimeRuleScheduler(this.settings.timeRules);
|
||||
this.styleManager = new StyleManager(this.app);
|
||||
this.persistence = new BackgroundPersistence(this.app);
|
||||
this.bgManager = new BackgroundManager(this.scheduler, this.styleManager, this.events, this, () => {
|
||||
void this.saveSettings();
|
||||
});
|
||||
|
||||
// 状态栏
|
||||
if (this.settings.statusBarEnabled) {
|
||||
|
|
@ -52,28 +57,24 @@ export default class DynamicThemeBackgroundPlugin extends Plugin {
|
|||
|
||||
// 启动背景管理器
|
||||
if (this.settings.enabled) {
|
||||
// 等待 layout-ready 事件,确保 vault 完全加载
|
||||
this.app.workspace.onLayoutReady(() => {
|
||||
this.startBackgroundManager();
|
||||
});
|
||||
}
|
||||
|
||||
// Wallpaper API 管理器
|
||||
// 实例化所有已配置的API
|
||||
for (const apiConfig of this.settings.wallpaperApis) {
|
||||
void apiManager.createApi(apiConfig);
|
||||
}
|
||||
|
||||
console.debug("Dynamic Theme Background plugin loaded");
|
||||
logger.debug("Plugin loaded");
|
||||
}
|
||||
|
||||
onunload() {
|
||||
this.stopBackgroundManager();
|
||||
|
||||
// 清理所有注册的API实例(包括状态管理器中的所有订阅)
|
||||
apiManager.deleteAllApis();
|
||||
|
||||
console.debug("Dynamic Theme Background plugin unloaded");
|
||||
this.events.removeAllListeners();
|
||||
logger.debug("Plugin unloaded");
|
||||
}
|
||||
|
||||
async loadSettings() {
|
||||
|
|
@ -86,39 +87,144 @@ export default class DynamicThemeBackgroundPlugin extends Plugin {
|
|||
await this.saveData(this.settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* 激活设置标签页视图
|
||||
*/
|
||||
activateView(): void {
|
||||
// 检查是否已经存在该类型的 leaf
|
||||
const existingLeaf = this.app.workspace.getLeavesOfType(DTB_SETTINGS_VIEW_TYPE)[0];
|
||||
// ============================================================================
|
||||
// 背景管理器代理
|
||||
// ============================================================================
|
||||
|
||||
get background(): BackgroundItem | null {
|
||||
return this.bgManager?.background ?? null;
|
||||
}
|
||||
|
||||
set background(bg: BackgroundItem | null) {
|
||||
if (this.bgManager) {
|
||||
this.bgManager.background = bg;
|
||||
}
|
||||
}
|
||||
|
||||
startBackgroundManager() {
|
||||
this.scheduler.updateRules(this.settings.timeRules);
|
||||
this.bgManager.start(this.settings);
|
||||
}
|
||||
|
||||
stopBackgroundManager() {
|
||||
this.bgManager?.stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* 按设定规则更新背景
|
||||
*/
|
||||
async updateBackground(forceUpdate = true) {
|
||||
await this.bgManager.update(this.settings, forceUpdate);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新样式 CSS(代理到 StyleManager)
|
||||
*/
|
||||
updateStyleCss(bgSize?: string) {
|
||||
this.styleManager.applyBackground(this.background, this.settings, bgSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步应用随机壁纸
|
||||
*/
|
||||
async applyRandomWallpaper(): Promise<void> {
|
||||
await this.bgManager.applyRandom(this.settings);
|
||||
this.refreshActiveBackgrounds();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 背景保存
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* 保存当前背景设置
|
||||
*/
|
||||
async saveBackground(bg: BackgroundItem | null = this.background) {
|
||||
if (!bg) return;
|
||||
if (!this.settings.localBackgroundFolder) {
|
||||
new Notice(t("notice_save_background_valid_folder_path_required"));
|
||||
return;
|
||||
}
|
||||
if (bg.type !== "image") {
|
||||
new Notice(t("notice_save_background_only_image_supported"));
|
||||
return;
|
||||
}
|
||||
if (!this.styleManager.isRemoteImage(bg.value)) {
|
||||
new Notice(t("notice_save_background_no_need_save_local"));
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await this.persistence.saveRemoteImage(bg, this.settings.localBackgroundFolder);
|
||||
if (!result.success) {
|
||||
if (result.error !== "cancelled") {
|
||||
new Notice(t("notice_save_background_failed"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新 settings 中的 bg 引用(不可变更新)
|
||||
const updatedBg = result.updatedBg;
|
||||
const bgIndex = this.settings.backgrounds.findIndex((item) => item.id === updatedBg.id);
|
||||
if (bgIndex >= 0) {
|
||||
this.settings.backgrounds[bgIndex] = updatedBg;
|
||||
} else {
|
||||
this.settings.backgrounds.push(updatedBg);
|
||||
this.refreshActiveBackgrounds();
|
||||
this.refreshActiveTimeRules();
|
||||
}
|
||||
|
||||
// 更新当前显示的背景
|
||||
if (this.background?.id === updatedBg.id) {
|
||||
this.bgManager.background = updatedBg;
|
||||
}
|
||||
|
||||
await this.saveSettings();
|
||||
new Notice(t("notice_save_background_success", { folderPath: this.settings.localBackgroundFolder }));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 工具方法(代理)
|
||||
// ============================================================================
|
||||
|
||||
getBgURL(bg: BackgroundItem): string {
|
||||
return this.styleManager.getBgURL(bg);
|
||||
}
|
||||
|
||||
isRemoteImage(imagePath: string): boolean {
|
||||
return this.styleManager.isRemoteImage(imagePath);
|
||||
}
|
||||
|
||||
getCurrentTimeRule() {
|
||||
return this.scheduler.getCurrentRule();
|
||||
}
|
||||
|
||||
getNextRuleChangeTime() {
|
||||
return this.scheduler.getNextChangeTime();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 界面方法
|
||||
// ============================================================================
|
||||
|
||||
activateView(): void {
|
||||
const existingLeaf = this.app.workspace.getLeavesOfType(DTB_SETTINGS_VIEW_TYPE)[0];
|
||||
if (existingLeaf) {
|
||||
// 如果已存在,则聚焦它
|
||||
void this.app.workspace.revealLeaf(existingLeaf);
|
||||
} else {
|
||||
// 如果不存在,则创建新的 leaf
|
||||
const leaf = this.app.workspace.getLeaf("tab");
|
||||
void leaf.setViewState({
|
||||
type: DTB_SETTINGS_VIEW_TYPE,
|
||||
active: true,
|
||||
});
|
||||
|
||||
// 确保标签页获得焦点
|
||||
void this.app.workspace.revealLeaf(leaf);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止状态栏
|
||||
*/
|
||||
deactivateStatusBar() {
|
||||
this.statusBar?.empty();
|
||||
this.statusBar?.remove();
|
||||
this.statusBar = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 激活状态栏,左键点击切换随机壁纸,右键点击保存当前背景,中键打开设置标签页
|
||||
*/
|
||||
activateStatusBar() {
|
||||
this.deactivateStatusBar();
|
||||
this.statusBar = this.addStatusBarItem();
|
||||
|
|
@ -141,661 +247,43 @@ export default class DynamicThemeBackgroundPlugin extends Plugin {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止背景管理器
|
||||
*/
|
||||
stopBackgroundManager() {
|
||||
if (this.intervalId) {
|
||||
clearInterval(this.intervalId);
|
||||
this.intervalId = null;
|
||||
}
|
||||
if (this.timeoutId) {
|
||||
clearTimeout(this.timeoutId);
|
||||
this.timeoutId = null;
|
||||
}
|
||||
document.body.classList.remove("dtb-enabled");
|
||||
console.debug("DTB: Background manager stopped");
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动背景管理器
|
||||
*/
|
||||
startBackgroundManager() {
|
||||
this.stopBackgroundManager();
|
||||
|
||||
// 如果没有 'dtb-enabled' 类,则添加
|
||||
if (!document.body.classList.contains("dtb-enabled")) {
|
||||
document.body.classList.add("dtb-enabled");
|
||||
}
|
||||
|
||||
// 立即执行一次更新
|
||||
void this.updateBackground(true);
|
||||
|
||||
if (this.settings.mode === "time-based") {
|
||||
// 时段模式:使用 setTimeout,计算到下一个时段的时间
|
||||
this.startTimeBasedManager();
|
||||
} else if (this.settings.mode === "interval") {
|
||||
// 间隔模式:使用 setInterval
|
||||
const intervalMs = this.settings.intervalMinutes * 60000;
|
||||
this.intervalId = this.registerInterval(
|
||||
window.setInterval(() => {
|
||||
void this.updateBackground(false);
|
||||
}, intervalMs)
|
||||
);
|
||||
|
||||
console.debug("DTB: Background manager started (interval mode)", {
|
||||
mode: this.settings.mode,
|
||||
interval: intervalMs / 1000 + "s",
|
||||
});
|
||||
} else {
|
||||
console.debug("DTB: Background manager started (manual mode)", {
|
||||
mode: this.settings.mode,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 时段规则下的背景更新循环,通过 setTimeout 实现
|
||||
startTimeBasedManager(): void {
|
||||
const scheduleNext = () => {
|
||||
const nextRuleChange = this.getNextRuleChangeTime();
|
||||
if (nextRuleChange) {
|
||||
const delay = nextRuleChange - Date.now();
|
||||
|
||||
// 确保延迟时间为正数,最少1秒
|
||||
const actualDelay = Math.max(delay, 1000);
|
||||
|
||||
this.timeoutId = window.setTimeout(() => {
|
||||
void this.updateBackground(false);
|
||||
// 此处应该刷新设置页中的时间规则列表
|
||||
this.refreshActiveTimeRules();
|
||||
scheduleNext(); // 递归调度下一个时段
|
||||
}, actualDelay);
|
||||
|
||||
console.debug("DTB: Next background change scheduled", {
|
||||
mode: this.settings.mode,
|
||||
delay: Math.round(actualDelay / 1000) + "s",
|
||||
nextTime: new Date(nextRuleChange).toLocaleTimeString(),
|
||||
});
|
||||
} else {
|
||||
// 如果没有下一个时段,24小时后重新检查
|
||||
this.timeoutId = window.setTimeout(
|
||||
() => {
|
||||
void this.updateBackground(false);
|
||||
scheduleNext();
|
||||
},
|
||||
24 * 60 * 60 * 1000
|
||||
);
|
||||
|
||||
console.debug("DTB: No next rule found, checking again in 24 hours");
|
||||
}
|
||||
};
|
||||
|
||||
scheduleNext();
|
||||
}
|
||||
|
||||
/**
|
||||
* 按设定规则更新背景
|
||||
* @param forceUpdate 是否强制更新背景
|
||||
*/
|
||||
async updateBackground(forceUpdate = true) {
|
||||
if (!this.settings.enabled) return;
|
||||
|
||||
let needsUpdate = false;
|
||||
switch (this.settings.mode) {
|
||||
// 基于时段切换
|
||||
case "time-based": {
|
||||
const rule = this.getCurrentTimeRule();
|
||||
if (rule) {
|
||||
// 判断是否与当前背景不同
|
||||
needsUpdate = this.background?.id !== rule.backgroundId;
|
||||
if (needsUpdate) {
|
||||
this.background = this.settings.backgrounds.find((bg) => bg.id === rule.backgroundId) ?? null;
|
||||
}
|
||||
} else {
|
||||
// 当前没有匹配的时段规则,取消背景
|
||||
this.background = null;
|
||||
needsUpdate = true; // 强制更新样式以清除背景
|
||||
}
|
||||
// 调试信息, 降低等级,避免刷屏
|
||||
console.debug("DTB: TimeRule mode - current time rule", rule, needsUpdate);
|
||||
break;
|
||||
}
|
||||
// 基于时间间隔切换
|
||||
case "interval": {
|
||||
const randomBg = await this.fetchRandomWallpaperFromAPI();
|
||||
if (randomBg) {
|
||||
this.background = randomBg;
|
||||
needsUpdate = true;
|
||||
} else if (this.settings.backgrounds.length > 0) {
|
||||
// API失败时回退到本地背景
|
||||
this.settings.currentIndex = (this.settings.currentIndex + 1) % this.settings.backgrounds.length;
|
||||
this.background = this.settings.backgrounds[this.settings.currentIndex];
|
||||
void this.saveSettings();
|
||||
needsUpdate = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
// 手动模式
|
||||
this.background = this.settings.backgrounds[this.settings.currentIndex];
|
||||
}
|
||||
}
|
||||
|
||||
if (forceUpdate || needsUpdate) {
|
||||
this.updateStyleCss();
|
||||
// 这里需要刷新设置页面,以正确显示激活图标
|
||||
this.refreshActiveBackgrounds();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新样式 CSS(真正更新背景的地方), 调用前请确保已设置 this.background
|
||||
* @param bgSize 可选的自定义背景尺寸,如果不提供则自动计算
|
||||
*/
|
||||
updateStyleCss(bgSize?: string) {
|
||||
if (!this.settings.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.background) {
|
||||
// 没有激活背景时,仍然更新遮罩变量,保证设置页调整立即生效
|
||||
const bgColorLight = hexToRgba(this.settings.bgColorLight, this.settings.bgColorOpacityLight);
|
||||
const bgColorDark = hexToRgba(this.settings.bgColorDark, this.settings.bgColorOpacityDark);
|
||||
document.documentElement.setCssProps({
|
||||
"--dtb-bg-image": "none",
|
||||
"--dtb-bg-color-light": bgColorLight,
|
||||
"--dtb-bg-color-dark": bgColorDark,
|
||||
});
|
||||
} else {
|
||||
const bgCssValue =
|
||||
this.background.type === "image" ? this.getBgURL(this.background) : this.background.value;
|
||||
// 模糊度、亮度、饱和度、遮罩颜色和透明度、填充方式的优先级统一为:
|
||||
// 传入的自定义值 > 背景单独的设置 > 全局默认设置
|
||||
const blurDepth = this.background.blurDepth ?? this.settings.blurDepth;
|
||||
const brightness4Bg = this.background.brightness4Bg ?? this.settings.brightness4Bg;
|
||||
const saturate4Bg = this.background.saturate4Bg ?? this.settings.saturate4Bg;
|
||||
// 计算遮罩颜色与透明度(优先级:每背景按主题覆盖 > 全局 light/dark)
|
||||
const perBgLightColor = this.background.bgColorLight;
|
||||
const perBgLightOpacity = this.background.bgColorOpacityLight;
|
||||
const perBgDarkColor = this.background.bgColorDark;
|
||||
const perBgDarkOpacity = this.background.bgColorOpacityDark;
|
||||
|
||||
const lightColor = perBgLightColor ?? this.settings.bgColorLight;
|
||||
const lightOpacity = perBgLightOpacity ?? this.settings.bgColorOpacityLight;
|
||||
const darkColor = perBgDarkColor ?? this.settings.bgColorDark;
|
||||
const darkOpacity = perBgDarkOpacity ?? this.settings.bgColorOpacityDark;
|
||||
const bgColorLight = hexToRgba(lightColor, lightOpacity);
|
||||
const bgColorDark = hexToRgba(darkColor, darkOpacity);
|
||||
bgSize = bgSize ?? this.background.bgSize ?? this.settings.bgSize ?? "intelligent";
|
||||
// 如果是 "intelligent",则根据图片和屏幕比例动态选择
|
||||
if (bgSize === "intelligent") {
|
||||
if (this.background.type === "image") {
|
||||
bgSize = this.getOptimalBackgroundSize(this.background.value);
|
||||
} else {
|
||||
bgSize = "auto"; // 对于非图片背景,使用 auto
|
||||
}
|
||||
}
|
||||
// 通过修改根元素的 CSS 属性来更新背景样式
|
||||
document.documentElement.setCssProps({
|
||||
"--dtb-bg-image": bgCssValue,
|
||||
"--dtb-blur-depth": `${blurDepth}px`,
|
||||
"--dtb-brightness": `${brightness4Bg}`,
|
||||
"--dtb-saturate": `${saturate4Bg}`,
|
||||
// 仅设置明/暗两套变量;统一变量 --dtb-bg-color 交由 CSS 中的 .theme-light/.theme-dark 进行映射
|
||||
"--dtb-bg-color-light": bgColorLight,
|
||||
"--dtb-bg-color-dark": bgColorDark,
|
||||
"--dtb-bg-size": bgSize,
|
||||
});
|
||||
}
|
||||
|
||||
// 通知 css-change
|
||||
this.app.workspace.trigger("css-change", { source: "dtb" });
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 辅助方法
|
||||
// 设置页刷新(保持兼容,后续 R3 将替换为纯事件驱动)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* 根据图片和屏幕比例动态选择最佳的background-size
|
||||
* @param imagePath 图片路径
|
||||
* @returns 最佳的background-size值
|
||||
*/
|
||||
private getOptimalBackgroundSize(imagePath: string): string {
|
||||
// 对于远程图片,由于无法同步获取尺寸,使用contain作为默认值
|
||||
if (imagePath.startsWith("http://") || imagePath.startsWith("https://")) {
|
||||
return "contain";
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取屏幕比例
|
||||
const screenRatio = window.innerWidth / window.innerHeight;
|
||||
|
||||
// 对于本地图片,尝试获取图片尺寸
|
||||
const file = this.app.vault.getFileByPath(imagePath);
|
||||
if (!file) {
|
||||
return "contain";
|
||||
}
|
||||
|
||||
// 获取资源路径
|
||||
const resourcePath = this.app.vault.getResourcePath(file);
|
||||
|
||||
if (!resourcePath) {
|
||||
return "contain";
|
||||
}
|
||||
|
||||
// 异步加载图片并更新尺寸
|
||||
void this.loadImageAndUpdateSize(resourcePath, screenRatio);
|
||||
|
||||
return "contain"; // 默认返回contain,异步更新后会重新渲染
|
||||
} catch (error) {
|
||||
console.warn("DTB: Error determining optimal background size:", error);
|
||||
return "contain";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步加载图片并根据比例更新background-size
|
||||
* @param resourcePath 图片资源路径
|
||||
* @param screenRatio 屏幕比例
|
||||
*/
|
||||
private async loadImageAndUpdateSize(resourcePath: string, screenRatio: number) {
|
||||
try {
|
||||
const img = new Image();
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
img.onload = () => resolve();
|
||||
img.onerror = () => reject(new Error("Failed to load image"));
|
||||
img.src = resourcePath;
|
||||
});
|
||||
|
||||
const imageRatio = img.naturalWidth / img.naturalHeight;
|
||||
|
||||
// 计算比例差异
|
||||
const ratioDifference = Math.abs(imageRatio - screenRatio) / screenRatio;
|
||||
|
||||
let optimalSize: string;
|
||||
|
||||
if (ratioDifference < 0.1) {
|
||||
// 比例相近(差异小于10%),使用cover以获得最佳填充效果
|
||||
optimalSize = "cover";
|
||||
} else if (imageRatio > screenRatio) {
|
||||
// 图片更宽,使用contain以显示完整宽度
|
||||
optimalSize = "contain";
|
||||
} else {
|
||||
// 图片更高,根据差异程度选择
|
||||
if (ratioDifference > 0.5) {
|
||||
// 差异很大,使用contain以确保完整显示
|
||||
optimalSize = "contain";
|
||||
} else {
|
||||
// 差异适中,使用cover以获得更好的视觉效果
|
||||
optimalSize = "cover";
|
||||
}
|
||||
}
|
||||
|
||||
console.debug(
|
||||
`DTB: Image analysis - Screen ratio: ${screenRatio.toFixed(2)}, Image ratio: ${imageRatio.toFixed(
|
||||
2
|
||||
)}, Optimal size: ${optimalSize}`
|
||||
);
|
||||
|
||||
// 使用优化后的方法更新样式(如果背景仍然是当前图片)
|
||||
if (this.background && this.background.type === "image") {
|
||||
this.updateStyleCss(optimalSize);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("DTB: Error loading image for size calculation:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步应用随机壁纸作为背景。
|
||||
*
|
||||
* 此方法会尝试从 API 获取一个随机壁纸 URL,并将其设置为当前背景。
|
||||
* 如果 API 获取失败,则会回退到本地背景列表,按顺序切换到下一个背景,并保存设置。
|
||||
* 最后会更新样式的 CSS 以应用新的背景。
|
||||
*
|
||||
*/
|
||||
async applyRandomWallpaper(): Promise<void> {
|
||||
const bg = await this.fetchRandomWallpaperFromAPI();
|
||||
|
||||
if (bg) {
|
||||
this.background = bg;
|
||||
} else if (this.settings.backgrounds.length > 0) {
|
||||
// API失败时回退到本地背景
|
||||
this.settings.currentIndex = (this.settings.currentIndex + 1) % this.settings.backgrounds.length;
|
||||
this.background = this.settings.backgrounds[this.settings.currentIndex];
|
||||
await this.saveSettings();
|
||||
}
|
||||
|
||||
this.updateStyleCss();
|
||||
// 这里需要刷新设置页面
|
||||
this.refreshActiveBackgrounds();
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存当前背景设置, 如果已经是本地图片则不操作
|
||||
*/
|
||||
async saveBackground(bg: BackgroundItem | null = this.background) {
|
||||
if (!bg) return;
|
||||
// 判断是否设置了 localBackgroundFolder
|
||||
if (!this.settings.localBackgroundFolder) {
|
||||
new Notice(t("notice_save_background_valid_folder_path_required"));
|
||||
return;
|
||||
}
|
||||
// 判断是否是图片,非图片则不保存
|
||||
if (bg.type !== "image") {
|
||||
new Notice(t("notice_save_background_only_image_supported"));
|
||||
return;
|
||||
}
|
||||
// 如果是本地图片,则不保存
|
||||
if (!this.isRemoteImage(bg.value)) {
|
||||
new Notice(t("notice_save_background_no_need_save_local"));
|
||||
return;
|
||||
}
|
||||
|
||||
// 保存远程图片
|
||||
const success = await this.saveRemoteImage(bg, this.settings.localBackgroundFolder);
|
||||
if (!success) {
|
||||
new Notice(t("notice_save_background_failed"));
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果本 bg 还未在列表中,则添加
|
||||
if (!this.settings.backgrounds.find((item) => item.id === bg.id)) {
|
||||
this.settings.backgrounds.push(bg);
|
||||
// 这里需要刷新设置页面中的背景列表和时间规则
|
||||
this.refreshActiveBackgrounds();
|
||||
this.refreshActiveTimeRules();
|
||||
}
|
||||
|
||||
// 由于保存远程图片时会将本地图片路径替换为远程路径,因此需要更新设置
|
||||
await this.saveSettings();
|
||||
|
||||
new Notice(t("notice_save_background_success", { folderPath: this.settings.localBackgroundFolder }));
|
||||
}
|
||||
|
||||
async saveRemoteImage(bg: BackgroundItem, folderPath: string): Promise<boolean> {
|
||||
if (!folderPath) {
|
||||
new Notice(t("notice_save_background_valid_folder_path_required"));
|
||||
return false;
|
||||
}
|
||||
|
||||
// 默认图片名为 bg.name + .jpg , 并规范化路径 移除禁止的字符: \ / : * ? " < > |
|
||||
const imageName = bg.name.replace(/[\\/:*?"<>|]/g, "_") + ".jpg";
|
||||
const localPath = `${folderPath}/${imageName}`;
|
||||
|
||||
// 判断路径是否存在,如果存在,由用户确定是否覆盖
|
||||
const file = this.app.vault.getFileByPath(localPath);
|
||||
if (file) {
|
||||
const overwrite = await confirm(
|
||||
this.app,
|
||||
t("notice_save_background_overwrite_existing_file", { filePath: localPath })
|
||||
);
|
||||
if (!overwrite) return true; // 用户取消覆盖
|
||||
}
|
||||
|
||||
// 这里添加保存远程图片的逻辑
|
||||
const response = await requestUrl({ url: bg.value });
|
||||
if (response.status < 200 || response.status >= 300) {
|
||||
new Notice(t("notice_save_background_failed"), response.status);
|
||||
return false;
|
||||
}
|
||||
const arrayBuffer = response.arrayBuffer;
|
||||
|
||||
// 如果需要覆盖,则先删除旧文件
|
||||
if (file) {
|
||||
await this.app.fileManager.trashFile(file);
|
||||
}
|
||||
await this.app.vault.createBinary(localPath, arrayBuffer);
|
||||
|
||||
// 默认将bg中的url替换为本地路径,并将remoteUrl设置为原始url以作备份
|
||||
bg.remoteUrl = bg.value;
|
||||
bg.value = localPath;
|
||||
// 这里提示用户转换了 value, 时间长点
|
||||
new Notice(t("notice_save_background_converted", { oldPath: bg.remoteUrl, newPath: bg.value }), 5000);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// 从壁纸API获取随机图片URL
|
||||
async fetchRandomWallpaperFromAPI(): Promise<BackgroundItem | null> {
|
||||
if (!this.settings.enableRandomWallpaper) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 获取所有启用的API实例
|
||||
const enabledApis = apiManager.getEnabledApis();
|
||||
// 如果没有启用的API,返回null
|
||||
if (enabledApis.length === 0) {
|
||||
console.warn("DTB: No enabled APIs found");
|
||||
return null;
|
||||
}
|
||||
// 从启用的API中随机选择一个
|
||||
const randomIndex = Math.floor(Math.random() * enabledApis.length);
|
||||
const selectedApi = enabledApis[randomIndex];
|
||||
|
||||
try {
|
||||
// 显示加载提示
|
||||
const loadingNotice = new Notice(t("notice_api_fetching", { apiName: selectedApi.getName() }), 0);
|
||||
|
||||
// 使用选中的API获取壁纸
|
||||
const wallpaperImages = await apiManager.getRandomWallpapers(selectedApi.getId());
|
||||
|
||||
// 关闭加载提示
|
||||
loadingNotice.hide();
|
||||
|
||||
if (!wallpaperImages || wallpaperImages.length === 0) {
|
||||
console.warn(`DTB: No images returned from API: ${selectedApi.getName()}`);
|
||||
return null;
|
||||
}
|
||||
const randomImage = wallpaperImages[Math.floor(Math.random() * wallpaperImages.length)];
|
||||
if (randomImage && randomImage.url) {
|
||||
new Notice(t("notice_api_success_applied", { apiName: selectedApi.getName() }));
|
||||
return {
|
||||
id: selectedApi.generateBackgroundId(),
|
||||
name: selectedApi.generateBackgroundName(),
|
||||
type: "image",
|
||||
value: randomImage.url,
|
||||
};
|
||||
} else {
|
||||
new Notice(t("notice_api_failed_fetch", { apiName: selectedApi.getName() }));
|
||||
console.warn(`DTB: No wallpaper image returned from API: ${selectedApi.getName()}`);
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
new Notice(t("notice_api_error_fetch", { apiName: selectedApi.getName(), error: error.message }));
|
||||
console.error("DTB: Error fetching random wallpaper:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 将图片路径转换为可用的 CSS URL
|
||||
getBgURL(bg: BackgroundItem): string {
|
||||
const imagePath = bg.value;
|
||||
// 判断是否是远程图片
|
||||
if (this.isRemoteImage(imagePath)) {
|
||||
return `url("${imagePath}")`;
|
||||
}
|
||||
// 本地图片路径(只接受 Vault 内的图片)
|
||||
const file = this.app.vault.getFileByPath(imagePath);
|
||||
if (file) {
|
||||
const p = this.app.vault.getResourcePath(file);
|
||||
if (p) {
|
||||
return `url("${p}")`;
|
||||
} else {
|
||||
console.warn(`DTB: Resource path for ${imagePath} is empty`);
|
||||
}
|
||||
} else {
|
||||
console.warn(`DTB: Image ${imagePath} not found or inaccessible`);
|
||||
}
|
||||
|
||||
// 如果 value 表示的本地路径无效,则查看 bg 有没有 remoteUrl 备份链接
|
||||
if (bg.remoteUrl) {
|
||||
// 这里恢复备份, 按理在这做不太合适
|
||||
bg.value = bg.remoteUrl;
|
||||
void this.saveSettings(); // 保存设置
|
||||
return `url("${bg.remoteUrl}")`;
|
||||
}
|
||||
|
||||
// 否则
|
||||
return "none";
|
||||
}
|
||||
|
||||
isRemoteImage(imagePath: string): boolean {
|
||||
return imagePath.startsWith("http://") || imagePath.startsWith("https://");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前时段规则
|
||||
* 按时间顺序排序规则,找到第一个匹配当前时间的规则
|
||||
*/
|
||||
getCurrentTimeRule(): TimeRule | null {
|
||||
if (this.settings.mode !== "time-based") return null;
|
||||
|
||||
const now = new Date();
|
||||
const currentTimeMinutes = now.getHours() * 60 + now.getMinutes();
|
||||
|
||||
// 按 startTime 排序,优先匹配靠前的规则
|
||||
const sortedRules = this.settings.timeRules
|
||||
.filter((rule) => rule.enabled)
|
||||
.map((rule) => {
|
||||
const { startTime, endTime } = this.parseTimeRule(rule);
|
||||
return { rule, startTime, endTime };
|
||||
})
|
||||
.sort((a, b) => a.startTime - b.startTime);
|
||||
|
||||
// 遍历排序后的规则,找到第一个匹配的
|
||||
for (const { rule, startTime, endTime } of sortedRules) {
|
||||
// 跨天时段处理:如 20:00-06:00
|
||||
if (startTime > endTime) {
|
||||
if (currentTimeMinutes >= startTime || currentTimeMinutes < endTime) {
|
||||
return rule;
|
||||
}
|
||||
} else {
|
||||
if (currentTimeMinutes >= startTime && currentTimeMinutes < endTime) {
|
||||
return rule;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取下一个时段规则变化的时间点
|
||||
*/
|
||||
getNextRuleChangeTime(): number | null {
|
||||
if (this.settings.mode !== "time-based" || this.settings.timeRules.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const currentTimeMinutes = now.getHours() * 60 + now.getMinutes();
|
||||
|
||||
// 收集所有启用的时段规则的开始和结束时间点
|
||||
const timePoints: Array<{ time: number; isStart: boolean; rule: TimeRule }> = [];
|
||||
|
||||
for (const rule of this.settings.timeRules) {
|
||||
if (!rule.enabled) continue;
|
||||
|
||||
const { startTime, endTime } = this.parseTimeRule(rule);
|
||||
|
||||
timePoints.push({ time: startTime, isStart: true, rule });
|
||||
timePoints.push({ time: endTime, isStart: false, rule });
|
||||
}
|
||||
|
||||
// 按时间排序
|
||||
timePoints.sort((a, b) => a.time - b.time);
|
||||
|
||||
// 查找下一个时间点
|
||||
let nextPoint = null;
|
||||
|
||||
// 首先查找今天剩余时间内的下一个时间点
|
||||
for (const point of timePoints) {
|
||||
if (point.time > currentTimeMinutes) {
|
||||
nextPoint = point;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果今天没有找到,取明天的第一个时间点
|
||||
if (!nextPoint && timePoints.length > 0) {
|
||||
nextPoint = timePoints[0];
|
||||
}
|
||||
|
||||
if (!nextPoint) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 计算具体的时间戳
|
||||
const targetDate = new Date(now);
|
||||
const targetHours = Math.floor(nextPoint.time / 60);
|
||||
const targetMinutes = nextPoint.time % 60;
|
||||
|
||||
targetDate.setHours(targetHours, targetMinutes, 0, 0);
|
||||
|
||||
// 如果目标时间已经过了,说明是明天的时间点
|
||||
if (targetDate.getTime() <= now.getTime()) {
|
||||
targetDate.setDate(targetDate.getDate() + 1);
|
||||
}
|
||||
|
||||
return targetDate.getTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析时间规则,返回标准化的时间信息
|
||||
*/
|
||||
private parseTimeRule(rule: TimeRule) {
|
||||
const [startHour, startMin] = rule.startTime.split(":").map(Number);
|
||||
const [endHour, endMin] = rule.endTime.split(":").map(Number);
|
||||
|
||||
const startTime = startHour * 60 + startMin;
|
||||
const endTime = endHour * 60 + endMin;
|
||||
|
||||
return { startTime, endTime };
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新当前激活的设置页
|
||||
*/
|
||||
refreshActiveSettings() {
|
||||
this.settingTabs.forEach((tab) => {
|
||||
if (tab.isActive()) {
|
||||
tab.display();
|
||||
}
|
||||
});
|
||||
this.events.emit("settings:changed", { key: "enabled", value: this.settings.enabled });
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新当前激活的设置页中的时间规则列表
|
||||
*/
|
||||
refreshActiveTimeRules() {
|
||||
this.settingTabs.forEach((tab) => {
|
||||
if (tab.isActive()) {
|
||||
tab.displayTimeRules();
|
||||
}
|
||||
});
|
||||
this.events.emit("time-rules:changed", {});
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新当前激活的设置页中的背景列表
|
||||
*/
|
||||
refreshActiveBackgrounds() {
|
||||
this.settingTabs.forEach((tab) => {
|
||||
if (tab.isActive()) {
|
||||
tab.displayBackgrounds();
|
||||
}
|
||||
});
|
||||
this.events.emit("backgrounds:changed", {});
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新当前激活的设置页中的api列表
|
||||
*/
|
||||
refreshActiveApiList() {
|
||||
this.settingTabs.forEach((tab) => {
|
||||
if (tab.isActive()) {
|
||||
tab.displayWallpaperApis();
|
||||
}
|
||||
});
|
||||
this.events.emit("apis:changed", {});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
382
src/settings/sections/api-settings.ts
Normal file
382
src/settings/sections/api-settings.ts
Normal file
|
|
@ -0,0 +1,382 @@
|
|||
/**
|
||||
* 壁纸 API 管理设置区段
|
||||
* 从 DTBSettingTab 中提取的独立 section 类,负责壁纸 API 的显示、添加、编辑、删除和拖拽排序
|
||||
*/
|
||||
import { Notice, Setting } from "obsidian";
|
||||
import { logger } from "../../core/logger";
|
||||
import { t } from "../../i18n";
|
||||
import { WallpaperApiEditorModal } from "../../modals";
|
||||
import type DynamicThemeBackgroundPlugin from "../../plugin";
|
||||
import type { BackgroundItem, DTBSettings } from "../../types";
|
||||
import { DragSort, generateId } from "../../utils";
|
||||
import {
|
||||
ApiStateSubscriber,
|
||||
BaseWallpaperApi,
|
||||
WallpaperApiConfig,
|
||||
WallpaperApiType,
|
||||
apiManager,
|
||||
} from "../../wallpaper-apis";
|
||||
|
||||
export class ApiSettingsSection {
|
||||
private plugin: DynamicThemeBackgroundPlugin;
|
||||
private defaultSettings: DTBSettings;
|
||||
|
||||
private componentId: string;
|
||||
private container!: HTMLElement;
|
||||
private apiListContainer!: HTMLElement;
|
||||
private apiDragSort?: DragSort<WallpaperApiConfig>;
|
||||
|
||||
/** 外部回调:刷新背景列表 */
|
||||
private onBackgroundsChanged?: () => void;
|
||||
/** 外部回调:刷新时间规则 */
|
||||
private onTimeRulesChanged?: () => void;
|
||||
|
||||
constructor(
|
||||
plugin: DynamicThemeBackgroundPlugin,
|
||||
defaultSettings: DTBSettings,
|
||||
callbacks?: {
|
||||
onBackgroundsChanged?: () => void;
|
||||
onTimeRulesChanged?: () => void;
|
||||
}
|
||||
) {
|
||||
this.plugin = plugin;
|
||||
this.defaultSettings = defaultSettings;
|
||||
this.componentId = generateId("api-settings-section");
|
||||
this.onBackgroundsChanged = callbacks?.onBackgroundsChanged;
|
||||
this.onTimeRulesChanged = callbacks?.onTimeRulesChanged;
|
||||
}
|
||||
|
||||
/*
|
||||
* 显示壁纸 API 管理设置
|
||||
*/
|
||||
display(container: HTMLElement): void {
|
||||
this.container = container;
|
||||
const containerEl = this.container;
|
||||
containerEl.empty();
|
||||
|
||||
// 壁纸API管理标题
|
||||
new Setting(containerEl).setName(t("wallpaper_api_management_title")).setHeading();
|
||||
|
||||
// 添加 API 按钮
|
||||
const buttonContainer = containerEl.createDiv("dtb-large-button-container");
|
||||
new Setting(buttonContainer)
|
||||
.setName(t("add_api_name"))
|
||||
.setDesc(t("add_api_desc"))
|
||||
.addButton((button) => {
|
||||
button.setButtonText(t("add_api_button"));
|
||||
button.onClick(() => this.showAddWallpaperApiModal());
|
||||
})
|
||||
// 添加新增所有默认 API 设置的恢复按钮,如果 API 已经存在则不添加
|
||||
.addExtraButton((button) => {
|
||||
button.setIcon("refresh-cw");
|
||||
button.setTooltip(t("restore_default_apis_tooltip"));
|
||||
button.onClick(() => {
|
||||
// 重新生成默认设置以获取最新的默认 API
|
||||
const defaultApis = this.defaultSettings.wallpaperApis;
|
||||
|
||||
// 遍历默认 API,检查是否已存在
|
||||
for (const apiConfig of defaultApis) {
|
||||
const existingApi = this.plugin.settings.wallpaperApis.find(
|
||||
(api) => api.id === apiConfig.id
|
||||
);
|
||||
if (!existingApi) {
|
||||
// 如果不存在,则添加并创建 API 实例
|
||||
this.plugin.settings.wallpaperApis.push(apiConfig);
|
||||
void apiManager.createApi(apiConfig);
|
||||
}
|
||||
}
|
||||
new Notice(t("restore_default_apis_success"));
|
||||
|
||||
void this.plugin.saveSettings();
|
||||
this.display(this.container);
|
||||
});
|
||||
});
|
||||
|
||||
// 添加 API 提示
|
||||
const hint = containerEl.createDiv("dtb-hint");
|
||||
hint.textContent = t("wallpaper_api_hint");
|
||||
|
||||
// 显示现有API列表
|
||||
this.apiListContainer = containerEl.createDiv("dtb-section-container");
|
||||
this.displayWallpaperApis();
|
||||
}
|
||||
|
||||
/*
|
||||
* 显示所有已配置的壁纸 API
|
||||
*/
|
||||
displayWallpaperApis(): void {
|
||||
const container = this.apiListContainer;
|
||||
this.apiDragSort?.disableAllDrag();
|
||||
container.empty();
|
||||
|
||||
// 初始化 API 拖拽排序
|
||||
this.apiDragSort = new DragSort<WallpaperApiConfig>({
|
||||
container,
|
||||
items: this.plugin.settings.wallpaperApis,
|
||||
getItemId: (api) => api.id,
|
||||
itemClass: "dtb-draggable",
|
||||
idDataAttribute: "apiId",
|
||||
onReorder: async (reorderedApis) => {
|
||||
this.plugin.settings.wallpaperApis = reorderedApis;
|
||||
await this.plugin.saveSettings();
|
||||
this.displayWallpaperApis();
|
||||
},
|
||||
});
|
||||
|
||||
// API 列表
|
||||
this.plugin.settings.wallpaperApis.forEach((apiConfig: WallpaperApiConfig, index: number) => {
|
||||
const apiInstance = apiManager.getApiById(apiConfig.id);
|
||||
if (!apiInstance) {
|
||||
logger.warn(`API instance not found for ${apiConfig.name}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const setting = new Setting(container).setName(apiConfig.name).setDesc(apiInstance.getDescription());
|
||||
|
||||
// 在设置项的控件区域直接添加类型标签
|
||||
setting.controlEl.createSpan({ text: apiConfig.type ?? "Unknown", cls: "dtb-badge" });
|
||||
|
||||
// 注意:状态指示器和启用按钮的状态都以 API 实例的状态为准,配置项的 enabled 字段仅用于初始状态和保存设置时的同步。
|
||||
|
||||
// 添加状态指示器
|
||||
const statusIndicator = setting.controlEl.createDiv("dtb-api-status");
|
||||
const statusDot = statusIndicator.createDiv("dtb-api-status-dot");
|
||||
const statusText = statusIndicator.createSpan();
|
||||
// 根据API的启用状态设置初始状态
|
||||
if (apiInstance.getEnabled()) {
|
||||
statusDot.addClass("enabled");
|
||||
statusText.textContent = t("status_enabled");
|
||||
} else {
|
||||
statusDot.addClass("disabled");
|
||||
statusText.textContent = t("status_disabled");
|
||||
}
|
||||
|
||||
// 创建 toggle 并保存引用
|
||||
let toggleComponent: { setValue: (value: boolean) => void; getValue: () => boolean } | null = null;
|
||||
setting.addToggle((toggle) => {
|
||||
toggleComponent = toggle; // 保存 toggle 引用
|
||||
const toggleEl = toggle.setValue(apiInstance.getEnabled());
|
||||
|
||||
// 使用智能API管理方法
|
||||
toggleEl.onChange(async (value) => {
|
||||
// 禁用toggle防止用户重复点击,并添加loading样式
|
||||
toggle.setDisabled(true);
|
||||
toggleEl.toggleEl.addClass("dtb-loading");
|
||||
|
||||
try {
|
||||
let success: boolean;
|
||||
|
||||
if (value) {
|
||||
// 使用智能启用方法
|
||||
success = await apiManager.enableApi(apiConfig.id);
|
||||
} else {
|
||||
// 使用智能禁用方法
|
||||
success = await apiManager.disableApi(apiConfig.id);
|
||||
}
|
||||
|
||||
const action = value ? t("action_enable") : t("action_disable");
|
||||
if (!success) {
|
||||
new Notice(
|
||||
t("notice_api_failed_enable_disable", { action, apiName: apiConfig.name }),
|
||||
3000
|
||||
);
|
||||
} else {
|
||||
new Notice(
|
||||
t("notice_api_success_enable_disable", { action, apiName: apiConfig.name }),
|
||||
3000
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Error ${value ? "enabling" : "disabling"} API:`, error);
|
||||
const action = value ? t("action_enable") : t("action_disable");
|
||||
new Notice(
|
||||
t("notice_api_error_enable_disable", { action, apiName: apiConfig.name }),
|
||||
3000
|
||||
);
|
||||
} finally {
|
||||
// 重新启用toggle并移除loading样式
|
||||
toggle.setDisabled(false);
|
||||
toggleEl.toggleEl.removeClass("dtb-loading");
|
||||
}
|
||||
});
|
||||
|
||||
return toggleEl;
|
||||
});
|
||||
|
||||
// 订阅状态变化,使用 ApiStateSubscriber 对象进行标识
|
||||
const subscriber = new ApiStateSubscriber("toggle", this.componentId, apiConfig.id);
|
||||
apiManager.stateManager.subscribe(subscriber, async (state) => {
|
||||
// 更新状态点的样式
|
||||
statusDot.removeClass("enabled", "disabled", "error", "loading");
|
||||
|
||||
if (state.isLoading) {
|
||||
statusDot.addClass("loading");
|
||||
statusText.textContent = t("status_loading");
|
||||
} else if (state.error) {
|
||||
statusDot.addClass("error");
|
||||
statusText.textContent = t("status_error");
|
||||
statusText.title = state.error;
|
||||
// 同步更新 toggle 状态
|
||||
if (toggleComponent && toggleComponent.getValue() !== false) {
|
||||
toggleComponent.setValue(false);
|
||||
}
|
||||
apiConfig.enabled = false;
|
||||
await this.plugin.saveSettings();
|
||||
} else if (state.instanceEnabled) {
|
||||
statusDot.addClass("enabled");
|
||||
statusText.textContent = t("status_enabled");
|
||||
// 同步更新 toggle 状态
|
||||
if (toggleComponent && toggleComponent.getValue() !== true) {
|
||||
toggleComponent.setValue(true);
|
||||
}
|
||||
apiConfig.enabled = true;
|
||||
await this.plugin.saveSettings();
|
||||
} else {
|
||||
statusDot.addClass("disabled");
|
||||
statusText.textContent = t("status_disabled");
|
||||
// 同步更新 toggle 状态
|
||||
if (toggleComponent && toggleComponent.getValue() !== false) {
|
||||
toggleComponent.setValue(false);
|
||||
}
|
||||
apiConfig.enabled = false;
|
||||
await this.plugin.saveSettings();
|
||||
}
|
||||
});
|
||||
|
||||
setting
|
||||
.addButton((button) =>
|
||||
button
|
||||
.setButtonText(t("button_add"))
|
||||
.setTooltip(t("add_api_bg_tooltip"))
|
||||
.onClick(async () => {
|
||||
await this.fetchWallpaperFromApi(apiInstance);
|
||||
})
|
||||
)
|
||||
.addButton((button) =>
|
||||
button.setButtonText(t("button_edit")).onClick(() => {
|
||||
this.showEditWallpaperApiModal(apiConfig, index);
|
||||
})
|
||||
)
|
||||
.addButton((button) =>
|
||||
button.setButtonText(t("button_delete")).onClick(() => {
|
||||
// 删除API实例
|
||||
apiManager.deleteApi(apiConfig.id);
|
||||
// 删除插件设置中的API配置
|
||||
this.plugin.settings.wallpaperApis = this.plugin.settings.wallpaperApis.filter(
|
||||
(api) => api.id !== apiConfig.id
|
||||
);
|
||||
void this.plugin.saveSettings();
|
||||
this.displayWallpaperApis();
|
||||
})
|
||||
);
|
||||
|
||||
// 设置拖拽属性
|
||||
setting.settingEl.addClass("dtb-draggable");
|
||||
setting.settingEl.dataset.apiId = apiConfig.id;
|
||||
|
||||
// 添加通用条目样式类
|
||||
setting.settingEl.addClass("dtb-button-container"); // 按钮样式
|
||||
|
||||
// 启用拖拽功能
|
||||
this.apiDragSort?.enableDragForElement(setting.settingEl, apiConfig);
|
||||
});
|
||||
}
|
||||
|
||||
// 显示添加壁纸API的模态窗口
|
||||
private showAddWallpaperApiModal() {
|
||||
const emptyConfig: WallpaperApiConfig = {
|
||||
id: "",
|
||||
name: "",
|
||||
type: WallpaperApiType.Custom,
|
||||
baseUrl: "",
|
||||
enabled: false,
|
||||
params: {},
|
||||
};
|
||||
|
||||
const modal = new WallpaperApiEditorModal(this.plugin.app, emptyConfig, (apiConfig) => {
|
||||
// 创建新的API实例
|
||||
void apiManager.createApi(apiConfig);
|
||||
// 添加到插件设置中
|
||||
this.plugin.settings.wallpaperApis.push(apiConfig);
|
||||
void this.plugin.saveSettings();
|
||||
// 这里仅需刷新 api 列表
|
||||
this.displayWallpaperApis();
|
||||
});
|
||||
|
||||
modal.open();
|
||||
}
|
||||
|
||||
// 显示编辑壁纸API的模态窗口
|
||||
private showEditWallpaperApiModal(apiConfig: WallpaperApiConfig, index: number) {
|
||||
const modal = new WallpaperApiEditorModal(this.plugin.app, apiConfig, (updatedConfig) => {
|
||||
// 有可能api类型也修改了,干脆重新创建API实例覆盖原来的
|
||||
void apiManager.createApi(updatedConfig);
|
||||
|
||||
this.plugin.settings.wallpaperApis[index] = updatedConfig;
|
||||
void this.plugin.saveSettings();
|
||||
// 这里仅需刷新 api 列表
|
||||
this.displayWallpaperApis();
|
||||
});
|
||||
|
||||
modal.open();
|
||||
}
|
||||
|
||||
// 从API获取壁纸并添加到背景列表
|
||||
private async fetchWallpaperFromApi(api: BaseWallpaperApi) {
|
||||
if (!api.getEnabled()) {
|
||||
new Notice(t("notice_api_disabled", { apiName: api.getName() }));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 显示加载提示
|
||||
const loadingNotice = new Notice(t("notice_api_fetching", { apiName: api.getName() }), 0);
|
||||
|
||||
// 使用API管理器获取随机壁纸
|
||||
const wallpaperImages = await apiManager.getRandomWallpapers(api.getId());
|
||||
|
||||
// 关闭加载提示
|
||||
loadingNotice.hide();
|
||||
|
||||
if (wallpaperImages) {
|
||||
// 创建新的图片背景项
|
||||
const newBg: BackgroundItem = {
|
||||
id: api.generateBackgroundId(),
|
||||
name: api.generateBackgroundName(),
|
||||
type: "image",
|
||||
value: wallpaperImages[0].url,
|
||||
};
|
||||
|
||||
// 添加到背景列表
|
||||
this.plugin.settings.backgrounds.push(newBg);
|
||||
await this.plugin.saveSettings();
|
||||
|
||||
// 立即应用这个背景
|
||||
this.plugin.background = newBg;
|
||||
this.plugin.updateStyleCss();
|
||||
|
||||
// 这里仅需刷新背景列表和时间规则
|
||||
this.onBackgroundsChanged?.();
|
||||
this.onTimeRulesChanged?.();
|
||||
|
||||
new Notice(t("notice_api_success_applied", { apiName: api.getName() }));
|
||||
} else {
|
||||
new Notice(t("notice_api_failed_fetch", { apiName: api.getName() }));
|
||||
}
|
||||
} catch (error) {
|
||||
new Notice(t("notice_api_error_fetch", { apiName: api.getName(), error: (error as Error).message }));
|
||||
logger.error("Error fetching wallpaper:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理工作
|
||||
*/
|
||||
cleanup(): void {
|
||||
// 清理拖拽排序实例
|
||||
this.apiDragSort?.disableAllDrag();
|
||||
|
||||
// 使用组件ID清理该组件的所有订阅
|
||||
apiManager.stateManager.cleanupByComponent(this.componentId);
|
||||
}
|
||||
}
|
||||
283
src/settings/sections/basic-settings.ts
Normal file
283
src/settings/sections/basic-settings.ts
Normal file
|
|
@ -0,0 +1,283 @@
|
|||
import { Setting } from "obsidian";
|
||||
import { t } from "../../i18n";
|
||||
import type DynamicThemeBackgroundPlugin from "../../plugin";
|
||||
import type { DTBSettings } from "../../types";
|
||||
import { addDropdownOptionHoverTooltip } from "../../utils";
|
||||
|
||||
export class BasicSettingsSection {
|
||||
private plugin: DynamicThemeBackgroundPlugin;
|
||||
private defaultSettings: DTBSettings;
|
||||
private container!: HTMLElement;
|
||||
|
||||
constructor(plugin: DynamicThemeBackgroundPlugin, defaultSettings: DTBSettings) {
|
||||
this.plugin = plugin;
|
||||
this.defaultSettings = defaultSettings;
|
||||
}
|
||||
|
||||
display(container: HTMLElement): void {
|
||||
this.container = container;
|
||||
const containerEl = this.container;
|
||||
containerEl.empty();
|
||||
|
||||
// 基础设置标题
|
||||
new Setting(containerEl).setName(t("basic_settings_title")).setHeading();
|
||||
|
||||
// 是否启用插件
|
||||
new Setting(containerEl)
|
||||
.setName(t("enable_plugin_name"))
|
||||
.setDesc(t("enable_plugin_desc"))
|
||||
.addToggle((toggle) =>
|
||||
toggle.setValue(this.plugin.settings.enabled).onChange(async (value) => {
|
||||
this.plugin.settings.enabled = value;
|
||||
await this.plugin.saveSettings();
|
||||
if (value) {
|
||||
this.plugin.startBackgroundManager();
|
||||
} else {
|
||||
this.plugin.stopBackgroundManager();
|
||||
}
|
||||
})
|
||||
)
|
||||
.addExtraButton((button) =>
|
||||
button
|
||||
.setIcon("refresh-cw")
|
||||
.setTooltip(t("reload_plugin_tooltip"))
|
||||
.onClick(() => {
|
||||
// 重新启动背景管理器
|
||||
if (this.plugin.settings.enabled) {
|
||||
this.plugin.startBackgroundManager();
|
||||
}
|
||||
// 强制更新当前背景
|
||||
void this.plugin.updateBackground(true);
|
||||
})
|
||||
);
|
||||
|
||||
// 是否开启状态栏
|
||||
new Setting(containerEl)
|
||||
.setName(t("enable_status_bar_name"))
|
||||
.setDesc(t("enable_status_bar_desc") + t("status_bar_title").replace(/\n/g, " "))
|
||||
.addToggle((toggle) =>
|
||||
toggle.setValue(this.plugin.settings.statusBarEnabled).onChange(async (value) => {
|
||||
this.plugin.settings.statusBarEnabled = value;
|
||||
await this.plugin.saveSettings();
|
||||
if (value) {
|
||||
this.plugin.activateStatusBar();
|
||||
} else {
|
||||
this.plugin.deactivateStatusBar();
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// 外观设置
|
||||
new Setting(containerEl).setName(t("appearance_settings_title")).setHeading();
|
||||
// 外观设置提示(全局外观优先级说明)
|
||||
const appearanceHint = containerEl.createDiv("dtb-hint");
|
||||
appearanceHint.textContent = t("appearance_settings_hint");
|
||||
// 背景模糊度设置
|
||||
new Setting(containerEl)
|
||||
.setName(t("blur_depth_name"))
|
||||
.setDesc(t("blur_depth_desc"))
|
||||
.addSlider((slider) =>
|
||||
slider
|
||||
.setLimits(0, 30, 1)
|
||||
.setValue(this.plugin.settings.blurDepth)
|
||||
.setDynamicTooltip()
|
||||
.onChange(async (value: number) => {
|
||||
this.plugin.settings.blurDepth = value;
|
||||
await this.plugin.saveSettings();
|
||||
this.plugin.updateStyleCss();
|
||||
})
|
||||
)
|
||||
.addExtraButton((button) =>
|
||||
button
|
||||
.setIcon("reset")
|
||||
.setTooltip(t("reset_blur_tooltip"))
|
||||
.onClick(async () => {
|
||||
this.plugin.settings.blurDepth = this.defaultSettings.blurDepth;
|
||||
await this.plugin.saveSettings();
|
||||
this.plugin.updateStyleCss();
|
||||
this.display(this.container);
|
||||
})
|
||||
);
|
||||
|
||||
// 背景亮度设置
|
||||
new Setting(containerEl)
|
||||
.setName(t("brightness_name"))
|
||||
.setDesc(t("brightness_desc"))
|
||||
.addSlider((slider) =>
|
||||
slider
|
||||
.setLimits(0, 1.5, 0.01)
|
||||
.setValue(this.plugin.settings.brightness4Bg)
|
||||
.setDynamicTooltip()
|
||||
.onChange(async (value: number) => {
|
||||
this.plugin.settings.brightness4Bg = value;
|
||||
await this.plugin.saveSettings();
|
||||
this.plugin.updateStyleCss();
|
||||
})
|
||||
)
|
||||
.addExtraButton((button) =>
|
||||
button
|
||||
.setIcon("reset")
|
||||
.setTooltip(t("reset_brightness_tooltip"))
|
||||
.onClick(async () => {
|
||||
this.plugin.settings.brightness4Bg = this.defaultSettings.brightness4Bg;
|
||||
await this.plugin.saveSettings();
|
||||
this.plugin.updateStyleCss();
|
||||
this.display(this.container);
|
||||
})
|
||||
);
|
||||
|
||||
// 背景饱和度设置
|
||||
new Setting(containerEl)
|
||||
.setName(t("saturate_name"))
|
||||
.setDesc(t("saturate_desc"))
|
||||
.addSlider((slider) =>
|
||||
slider
|
||||
.setLimits(0, 2, 0.01)
|
||||
.setValue(this.plugin.settings.saturate4Bg)
|
||||
.setDynamicTooltip()
|
||||
.onChange(async (value: number) => {
|
||||
this.plugin.settings.saturate4Bg = value;
|
||||
await this.plugin.saveSettings();
|
||||
this.plugin.updateStyleCss();
|
||||
})
|
||||
)
|
||||
.addExtraButton((button) =>
|
||||
button
|
||||
.setIcon("reset")
|
||||
.setTooltip(t("reset_saturate_tooltip"))
|
||||
.onClick(async () => {
|
||||
this.plugin.settings.saturate4Bg = this.defaultSettings.saturate4Bg;
|
||||
await this.plugin.saveSettings();
|
||||
this.plugin.updateStyleCss();
|
||||
this.display(this.container);
|
||||
})
|
||||
);
|
||||
|
||||
// 背景颜色和透明度设置(暗/亮两套,同一行:暗在前,亮在后)
|
||||
{
|
||||
const setting = new Setting(containerEl).setName(t("bg_mask_color_name")).setDesc(t("bg_mask_color_desc"));
|
||||
|
||||
// 暗主题标签
|
||||
const darkLabel = setting.controlEl.createSpan({ text: "🌙" });
|
||||
darkLabel.setAttribute("title", t("overlay_dark_tooltip"));
|
||||
|
||||
// 暗主题:颜色
|
||||
setting.addColorPicker((colorPicker) =>
|
||||
colorPicker
|
||||
.setValue(this.plugin.settings.bgColorDark ?? this.defaultSettings.bgColorDark)
|
||||
.onChange(async (value: string) => {
|
||||
this.plugin.settings.bgColorDark = value;
|
||||
await this.plugin.saveSettings();
|
||||
this.plugin.updateStyleCss();
|
||||
})
|
||||
);
|
||||
// 暗主题:透明度
|
||||
setting.addSlider((slider) =>
|
||||
slider
|
||||
.setLimits(0, 1, 0.01)
|
||||
.setValue(this.plugin.settings.bgColorOpacityDark ?? this.defaultSettings.bgColorOpacityDark)
|
||||
.setDynamicTooltip()
|
||||
.onChange(async (value: number) => {
|
||||
this.plugin.settings.bgColorOpacityDark = value;
|
||||
await this.plugin.saveSettings();
|
||||
this.plugin.updateStyleCss();
|
||||
})
|
||||
);
|
||||
|
||||
// 亮主题标签
|
||||
const lightLabel = setting.controlEl.createSpan({ text: "☀️" });
|
||||
lightLabel.setAttribute("title", t("overlay_light_tooltip"));
|
||||
|
||||
// 亮主题:颜色
|
||||
setting.addColorPicker((colorPicker) =>
|
||||
colorPicker
|
||||
.setValue(this.plugin.settings.bgColorLight ?? this.defaultSettings.bgColorLight)
|
||||
.onChange(async (value: string) => {
|
||||
this.plugin.settings.bgColorLight = value;
|
||||
await this.plugin.saveSettings();
|
||||
this.plugin.updateStyleCss();
|
||||
})
|
||||
);
|
||||
// 亮主题:透明度
|
||||
setting.addSlider((slider) =>
|
||||
slider
|
||||
.setLimits(0, 1, 0.01)
|
||||
.setValue(this.plugin.settings.bgColorOpacityLight ?? this.defaultSettings.bgColorOpacityLight)
|
||||
.setDynamicTooltip()
|
||||
.onChange(async (value: number) => {
|
||||
this.plugin.settings.bgColorOpacityLight = value;
|
||||
await this.plugin.saveSettings();
|
||||
this.plugin.updateStyleCss();
|
||||
})
|
||||
);
|
||||
|
||||
// 重置(两套新字段)
|
||||
setting.addExtraButton((button) =>
|
||||
button
|
||||
.setIcon("reset")
|
||||
.setTooltip(t("reset_bg_mask_color_tooltip"))
|
||||
.onClick(async () => {
|
||||
this.plugin.settings.bgColorDark = this.defaultSettings.bgColorDark;
|
||||
this.plugin.settings.bgColorOpacityDark = this.defaultSettings.bgColorOpacityDark;
|
||||
this.plugin.settings.bgColorLight = this.defaultSettings.bgColorLight;
|
||||
this.plugin.settings.bgColorOpacityLight = this.defaultSettings.bgColorOpacityLight;
|
||||
await this.plugin.saveSettings();
|
||||
this.plugin.updateStyleCss();
|
||||
this.display(this.container);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// 背景填充方式设置
|
||||
new Setting(containerEl)
|
||||
.setName(t("bg_size_name"))
|
||||
.setDesc(t("bg_size_desc"))
|
||||
.addDropdown((dropdown) => {
|
||||
// 添加下拉选项
|
||||
dropdown.addOption("intelligent", "Intelligent");
|
||||
dropdown.addOption("cover", "Cover");
|
||||
dropdown.addOption("contain", "Contain");
|
||||
dropdown.addOption("auto", "Auto");
|
||||
// 使用专门的悬停选项方法添加 tooltip(推荐用法)
|
||||
addDropdownOptionHoverTooltip(
|
||||
dropdown,
|
||||
{
|
||||
cover: t("bg_size_option_cover"),
|
||||
contain: t("bg_size_option_contain"),
|
||||
auto: t("bg_size_option_auto"),
|
||||
intelligent: t("bg_size_option_intelligent"),
|
||||
},
|
||||
{
|
||||
defaultTooltip: t("bg_size_desc"),
|
||||
updateOnChange: true, // 选择后也更新整个下拉框的 tooltip
|
||||
}
|
||||
);
|
||||
|
||||
dropdown
|
||||
.setValue(this.plugin.settings.bgSize)
|
||||
.onChange(async (value: string) => {
|
||||
const bgSize = value as DTBSettings["bgSize"];
|
||||
this.plugin.settings.bgSize = bgSize;
|
||||
await this.plugin.saveSettings();
|
||||
this.plugin.updateStyleCss();
|
||||
});
|
||||
|
||||
return dropdown;
|
||||
})
|
||||
.addExtraButton((button) =>
|
||||
button
|
||||
.setIcon("reset")
|
||||
.setTooltip(t("reset_bg_size_tooltip"))
|
||||
.onClick(async () => {
|
||||
this.plugin.settings.bgSize = this.defaultSettings.bgSize;
|
||||
await this.plugin.saveSettings();
|
||||
this.plugin.updateStyleCss();
|
||||
this.display(this.container);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
cleanup(): void {
|
||||
// Basic settings has no subscriptions to clean up
|
||||
}
|
||||
}
|
||||
448
src/settings/sections/bg-management.ts
Normal file
448
src/settings/sections/bg-management.ts
Normal file
|
|
@ -0,0 +1,448 @@
|
|||
/**
|
||||
* 背景管理区块
|
||||
* 从 DTBSettingTab 中提取的背景管理相关功能
|
||||
*/
|
||||
import { Notice, Setting, TFile } from "obsidian";
|
||||
import { logger } from "../../core/logger";
|
||||
import { t } from "../../i18n";
|
||||
import { BackgroundModal, ImageFolderSuggestModal } from "../../modals";
|
||||
import type DynamicThemeBackgroundPlugin from "../../plugin";
|
||||
import type { BackgroundItem, DTBSettings } from "../../types";
|
||||
import { DragSort, generateId } from "../../utils";
|
||||
|
||||
export class BgManagementSection {
|
||||
private plugin: DynamicThemeBackgroundPlugin;
|
||||
private defaultSettings: DTBSettings;
|
||||
|
||||
private container!: HTMLElement;
|
||||
private bgListContainer!: HTMLElement;
|
||||
private backgroundDragSort?: DragSort<BackgroundItem>;
|
||||
|
||||
/** Callback invoked when backgrounds change and external sections (e.g. time rules) need refresh */
|
||||
private onChanged?: () => void;
|
||||
|
||||
constructor(
|
||||
plugin: DynamicThemeBackgroundPlugin,
|
||||
defaultSettings: DTBSettings,
|
||||
options?: { onChanged?: () => void }
|
||||
) {
|
||||
this.plugin = plugin;
|
||||
this.defaultSettings = defaultSettings;
|
||||
this.onChanged = options?.onChanged;
|
||||
}
|
||||
|
||||
display(container: HTMLElement): void {
|
||||
this.container = container;
|
||||
const containerEl = this.container;
|
||||
containerEl.empty();
|
||||
|
||||
// 背景管理标题
|
||||
new Setting(containerEl).setName(t("bg_management_title")).setHeading();
|
||||
|
||||
// 保存远程图片的本地路径
|
||||
const imageFolderInputContainer = containerEl.createDiv("setting-item dtb-flex-container-spaced");
|
||||
imageFolderInputContainer.createEl("label", { text: t("save_image_path_title") });
|
||||
const valueInput = imageFolderInputContainer.createEl("input", {
|
||||
type: "text",
|
||||
title: t("save_image_path_title"),
|
||||
placeholder: t("save_image_path_placeholder"),
|
||||
value: this.plugin.settings.localBackgroundFolder ?? "",
|
||||
cls: "dtb-flex-1",
|
||||
});
|
||||
valueInput.oninput = () => {
|
||||
this.plugin.settings.localBackgroundFolder = valueInput.value;
|
||||
void this.plugin.saveSettings();
|
||||
};
|
||||
const browseButton = imageFolderInputContainer.createEl("button", {
|
||||
type: "button",
|
||||
text: t("button_browse"),
|
||||
cls: "dtb-button",
|
||||
});
|
||||
browseButton.onclick = () => {
|
||||
const modal = new ImageFolderSuggestModal(this.plugin.app, (imagePath: string) => {
|
||||
valueInput.value = imagePath;
|
||||
this.plugin.settings.localBackgroundFolder = imagePath;
|
||||
void this.plugin.saveSettings();
|
||||
});
|
||||
modal.open();
|
||||
};
|
||||
|
||||
// 添加背景的一组按钮
|
||||
const buttonContainer = containerEl.createDiv("dtb-large-button-container");
|
||||
new Setting(buttonContainer)
|
||||
.setName(t("add_new_bg_name"))
|
||||
.setDesc(t("add_new_bg_desc"))
|
||||
.addButton((button) =>
|
||||
button.setButtonText(t("add_image_bg_button")).onClick(() => this.showAddBackgroundModal("image"))
|
||||
)
|
||||
.addButton((button) =>
|
||||
button.setButtonText(t("add_color_bg_button")).onClick(() => this.showAddBackgroundModal("color"))
|
||||
)
|
||||
.addButton((button) =>
|
||||
button.setButtonText(t("add_gradient_bg_button")).onClick(() => this.showAddBackgroundModal("gradient"))
|
||||
)
|
||||
.addButton((button) =>
|
||||
button.setButtonText(t("add_folder_bg_button")).onClick(() => this.showAddFolderModal())
|
||||
)
|
||||
.addExtraButton((button) =>
|
||||
button
|
||||
.setIcon("reset")
|
||||
.setTooltip(t("restore_default_bg_tooltip"))
|
||||
.onClick(() => this.restoreDefaultBackgrounds())
|
||||
);
|
||||
|
||||
// 添加拖拽提示
|
||||
const dragHint = containerEl.createDiv("dtb-hint");
|
||||
dragHint.textContent = t("background_management_hint");
|
||||
|
||||
this.bgListContainer = containerEl.createDiv("dtb-section-container");
|
||||
this.displayBackgrounds();
|
||||
}
|
||||
|
||||
// 在指定的容器元素中渲染所有背景项
|
||||
displayBackgrounds(): void {
|
||||
const container = this.bgListContainer;
|
||||
this.backgroundDragSort?.disableAllDrag();
|
||||
container.empty();
|
||||
|
||||
// 初始化背景拖拽排序
|
||||
this.backgroundDragSort = new DragSort<BackgroundItem>({
|
||||
container,
|
||||
items: this.plugin.settings.backgrounds,
|
||||
getItemId: (bg) => bg.id,
|
||||
itemClass: "dtb-draggable",
|
||||
idDataAttribute: "bgId",
|
||||
onReorder: async (reorderedBackgrounds) => {
|
||||
this.plugin.settings.backgrounds = reorderedBackgrounds;
|
||||
await this.plugin.saveSettings();
|
||||
// 这里仅需刷新背景列表和时间规则列表
|
||||
this.displayBackgrounds();
|
||||
this.onChanged?.();
|
||||
},
|
||||
});
|
||||
this.plugin.settings.backgrounds.forEach((bg: BackgroundItem, index: number) => {
|
||||
const bgEl = container.createDiv("dtb-item dtb-draggable");
|
||||
|
||||
// 添加拖拽相关属性
|
||||
bgEl.draggable = true;
|
||||
bgEl.dataset.bgId = bg.id;
|
||||
bgEl.dataset.index = index.toString();
|
||||
|
||||
// 添加拖拽手柄
|
||||
const dragHandle = bgEl.createDiv("dtb-drag-handle");
|
||||
dragHandle.textContent = "⋮⋮"; // 使用双点符号作为拖拽手柄
|
||||
dragHandle.title = t("drag_handle_tooltip");
|
||||
|
||||
// 背景名称
|
||||
const contentDiv = bgEl.createDiv("dtb-bg-content");
|
||||
contentDiv.createSpan({ text: bg.name, cls: "dtb-bg-name" });
|
||||
|
||||
// 如果是启用背景,添加图标
|
||||
if (bg.id === this.plugin.background?.id) {
|
||||
const icon = contentDiv.createSpan();
|
||||
icon.setText("🔥");
|
||||
icon.title = t("current_background");
|
||||
}
|
||||
|
||||
// 背景类型
|
||||
contentDiv.createSpan({ text: bg.type, cls: "dtb-badge" });
|
||||
|
||||
// 预览图
|
||||
const preview = contentDiv.createDiv("dtb-bg-preview");
|
||||
this.setPreviewBackground(preview, bg);
|
||||
|
||||
// 操作按钮
|
||||
const actions = contentDiv.createDiv("dtb-button-container");
|
||||
|
||||
// 预览按钮
|
||||
actions.createEl("button", { text: t("button_preview") }).onclick = () => {
|
||||
this.plugin.background = bg;
|
||||
this.plugin.settings.currentIndex = index; // 更新当前索引
|
||||
void this.plugin.saveSettings();
|
||||
this.plugin.updateStyleCss();
|
||||
this.displayBackgrounds(); // 刷新激活图标
|
||||
};
|
||||
|
||||
// 保存按钮
|
||||
actions.createEl("button", { text: t("button_save") }).onclick = () => {
|
||||
void this.plugin.saveBackground(bg);
|
||||
};
|
||||
|
||||
// 编辑按钮
|
||||
actions.createEl("button", { text: t("button_edit") }).onclick = () => {
|
||||
this.showEditBackgroundModal(bg, index);
|
||||
};
|
||||
|
||||
// 删除按钮
|
||||
actions.createEl("button", { text: t("button_delete") }).onclick = () => {
|
||||
// 使用 filter 方法删除
|
||||
this.plugin.settings.backgrounds = this.plugin.settings.backgrounds.filter(
|
||||
(b: BackgroundItem) => b.id !== bg.id
|
||||
);
|
||||
void this.plugin.saveSettings();
|
||||
// 这里仅需刷新背景列表和时间规则列表
|
||||
this.displayBackgrounds();
|
||||
this.onChanged?.();
|
||||
};
|
||||
|
||||
// 启用拖拽功能
|
||||
this.backgroundDragSort?.enableDragForElement(bgEl, bg);
|
||||
});
|
||||
}
|
||||
|
||||
cleanup(): void {
|
||||
this.backgroundDragSort?.disableAllDrag();
|
||||
}
|
||||
|
||||
// 显示添加或编辑背景的模态窗口
|
||||
private showAddBackgroundModal(type: "image" | "color" | "gradient"): void {
|
||||
// 创建一个新的背景项,初始值为空
|
||||
const bg: BackgroundItem = {
|
||||
id: "",
|
||||
name: "",
|
||||
type,
|
||||
value: "",
|
||||
};
|
||||
const modal = new BackgroundModal(this.plugin.app, this.plugin, bg, (newBg: BackgroundItem) => {
|
||||
if (!newBg.name.trim() || !newBg.value.trim()) {
|
||||
new Notice(t("notice_name_and_value_required"));
|
||||
return;
|
||||
}
|
||||
newBg.name = newBg.name.trim();
|
||||
newBg.value = newBg.value.trim();
|
||||
|
||||
// 生成唯一ID
|
||||
newBg.id = generateId("bg");
|
||||
|
||||
// 添加到设置中
|
||||
this.plugin.settings.backgrounds.push(newBg);
|
||||
void this.plugin.saveSettings();
|
||||
|
||||
// 这里仅需刷新背景列表和时间规则列表
|
||||
this.displayBackgrounds();
|
||||
this.onChanged?.();
|
||||
});
|
||||
|
||||
modal.open();
|
||||
}
|
||||
|
||||
// 显示编辑背景的模态窗口
|
||||
private showEditBackgroundModal(bg: BackgroundItem, index: number): void {
|
||||
const modal = new BackgroundModal(this.plugin.app, this.plugin, bg, (newBg: BackgroundItem) => {
|
||||
if (!newBg.name.trim() || !newBg.value.trim()) {
|
||||
new Notice(t("notice_name_and_value_required"));
|
||||
return;
|
||||
}
|
||||
newBg.name = newBg.name.trim();
|
||||
newBg.value = newBg.value.trim();
|
||||
|
||||
// 更新现有背景项
|
||||
this.plugin.settings.backgrounds[index] = newBg;
|
||||
|
||||
void this.plugin.saveSettings();
|
||||
|
||||
// 如果当前正在使用这个背景,则更新显示
|
||||
if (this.plugin.background?.id === bg.id) {
|
||||
this.plugin.background = this.plugin.settings.backgrounds[index];
|
||||
this.plugin.updateStyleCss();
|
||||
}
|
||||
|
||||
// 这里仅需刷新背景列表和时间规则列表
|
||||
this.displayBackgrounds();
|
||||
this.onChanged?.();
|
||||
});
|
||||
|
||||
// 打开模态窗口(初始值已在 BackgroundModal.onOpen() 中从 bgItem 设置)
|
||||
modal.open();
|
||||
}
|
||||
|
||||
private showAddFolderModal(): void {
|
||||
const modal = new ImageFolderSuggestModal(this.plugin.app, (folderPath: string) => {
|
||||
if (!folderPath.trim()) {
|
||||
new Notice(t("notice_valid_folder_path_required"));
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查文件夹是否存在
|
||||
const folder = this.plugin.app.vault.getAbstractFileByPath(folderPath);
|
||||
if (!folder) {
|
||||
new Notice(t("notice_folder_not_found"));
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理文件夹中的图片文件
|
||||
this.addImagesFromFolder(folderPath)
|
||||
.then(() => {
|
||||
new Notice(t("notice_folder_added_successfully", { folderPath }));
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error("Error adding images from folder:", error);
|
||||
new Notice(t("notice_error_adding_folder_images"));
|
||||
});
|
||||
});
|
||||
|
||||
modal.open();
|
||||
}
|
||||
|
||||
private async restoreDefaultBackgrounds() {
|
||||
// 重新生成默认设置以获取最新的默认背景
|
||||
const defaultBackgrounds = this.defaultSettings.backgrounds;
|
||||
|
||||
let addedCount = 0;
|
||||
|
||||
// 遍历默认背景,只添加不存在的
|
||||
for (const defaultBg of defaultBackgrounds) {
|
||||
const existingBg = this.plugin.settings.backgrounds.find((bg) => bg.id === defaultBg.id);
|
||||
|
||||
if (!existingBg) {
|
||||
// 创建新的背景项,确保 ID 唯一
|
||||
const newBg: BackgroundItem = {
|
||||
id: defaultBg.id,
|
||||
name: defaultBg.name,
|
||||
type: defaultBg.type,
|
||||
value: defaultBg.value,
|
||||
};
|
||||
|
||||
this.plugin.settings.backgrounds.push(newBg);
|
||||
addedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (addedCount > 0) {
|
||||
await this.plugin.saveSettings();
|
||||
// 这里仅需刷新背景列表和时间规则列表
|
||||
this.displayBackgrounds();
|
||||
this.onChanged?.();
|
||||
new Notice(
|
||||
t("restore_default_bg_success", {
|
||||
count: addedCount.toString(),
|
||||
})
|
||||
);
|
||||
} else {
|
||||
new Notice(t("restore_default_bg_no_new"));
|
||||
}
|
||||
}
|
||||
|
||||
// 设置预览元素的背景样式 * 使用 CSS 自定义属性而不是内联样式,遵循 Obsidian 官方建议
|
||||
private setPreviewBackground(preview: HTMLElement, bg: BackgroundItem): void {
|
||||
// 移除之前的类型特定类名
|
||||
preview.removeClass("dtb-preview-image", "dtb-preview-color", "dtb-preview-gradient");
|
||||
|
||||
// 清除之前设置的 CSS 自定义属性
|
||||
preview.setCssProps({
|
||||
"--dtb-preview-bg-image": "",
|
||||
"--dtb-preview-bg": "",
|
||||
});
|
||||
|
||||
switch (bg.type) {
|
||||
case "image": {
|
||||
preview.addClass("dtb-preview-image");
|
||||
const sanitizedImagePath = this.plugin.getBgURL(bg);
|
||||
// 只有当图片路径有效时才设置 CSS 变量
|
||||
if (sanitizedImagePath && sanitizedImagePath !== "none") {
|
||||
preview.setCssProps({
|
||||
"--dtb-preview-bg-image": sanitizedImagePath,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "color":
|
||||
case "gradient": {
|
||||
preview.addClass(`dtb-preview-${bg.type}`);
|
||||
// 验证颜色/渐变值的有效性
|
||||
if (bg.value && bg.value.trim()) {
|
||||
preview.setCssProps({
|
||||
"--dtb-preview-bg": bg.value,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
logger.warn(`Unknown background type: ${bg.type as string}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 添加文件夹中的图片到背景列表
|
||||
private async addImagesFromFolder(folderPath: string) {
|
||||
try {
|
||||
// 标准化路径:移除开头和结尾的斜杠,只处理 vault 内的相对路径
|
||||
folderPath = folderPath.replace(/^\/+|\/+$/g, "");
|
||||
|
||||
let folderFiles: TFile[] = [];
|
||||
|
||||
if (folderPath !== "") {
|
||||
// 尝试获取指定文件夹
|
||||
const folder = this.plugin.app.vault.getFolderByPath(folderPath);
|
||||
if (folder) {
|
||||
// 只获取该文件夹下的直接子文件(不递归)
|
||||
folderFiles = this.plugin.app.vault.getFiles().filter((file) => {
|
||||
const fileDir = file.path.substring(0, file.path.lastIndexOf("/"));
|
||||
return fileDir === folderPath;
|
||||
});
|
||||
} else {
|
||||
new Notice(t("folder_not_found"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (folderFiles.length === 0) {
|
||||
new Notice(t("folder_not_found"));
|
||||
return;
|
||||
}
|
||||
|
||||
await this.processImageFiles(folderFiles, folderPath);
|
||||
} catch (error) {
|
||||
logger.error("Error scanning folder:", error);
|
||||
new Notice(t("folder_scan_error", { error: (error as Error).message }));
|
||||
}
|
||||
}
|
||||
|
||||
// 处理文件夹中的图片文件
|
||||
private async processImageFiles(files: TFile[], folderPath: string) {
|
||||
// 支持的图片格式
|
||||
const imageExtensions = [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".svg"];
|
||||
|
||||
// 过滤出图片文件
|
||||
const imageFiles = files.filter((file) => imageExtensions.some((ext) => file.path.toLowerCase().endsWith(ext)));
|
||||
|
||||
if (imageFiles.length === 0) {
|
||||
new Notice(t("folder_not_found"));
|
||||
return;
|
||||
}
|
||||
|
||||
let addedCount = 0;
|
||||
|
||||
for (const file of imageFiles) {
|
||||
// 检查是否已存在相同路径的背景
|
||||
const existingBg = this.plugin.settings.backgrounds.find(
|
||||
(bg) => bg.type === "image" && bg.value === file.path
|
||||
);
|
||||
|
||||
if (!existingBg) {
|
||||
const fileName = file.name.replace(/\.[^/.]+$/, ""); // 移除扩展名
|
||||
// 只保留最后一级文件夹名称,避免长路径影响观感
|
||||
const folderName = folderPath === "" ? "root" : (folderPath.split("/").pop() ?? folderPath);
|
||||
const newBg: BackgroundItem = {
|
||||
id: generateId("bg"),
|
||||
name: `${fileName} (${folderName})`,
|
||||
type: "image",
|
||||
value: file.path,
|
||||
};
|
||||
|
||||
this.plugin.settings.backgrounds.push(newBg);
|
||||
addedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (addedCount > 0) {
|
||||
await this.plugin.saveSettings();
|
||||
// 这里仅需刷新背景列表和时间规则列表
|
||||
this.displayBackgrounds();
|
||||
this.onChanged?.();
|
||||
new Notice(t("folder_scan_success", { count: addedCount.toString() }));
|
||||
} else {
|
||||
new Notice(t("folder_no_new_images"));
|
||||
}
|
||||
}
|
||||
}
|
||||
4
src/settings/sections/index.ts
Normal file
4
src/settings/sections/index.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export { ApiSettingsSection } from "./api-settings";
|
||||
export { BasicSettingsSection } from "./basic-settings";
|
||||
export { BgManagementSection } from "./bg-management";
|
||||
export { ModeSettingsSection } from "./mode-settings";
|
||||
285
src/settings/sections/mode-settings.ts
Normal file
285
src/settings/sections/mode-settings.ts
Normal file
|
|
@ -0,0 +1,285 @@
|
|||
import { Notice, Setting } from "obsidian";
|
||||
import { t } from "../../i18n";
|
||||
import { ConfirmModal, TimeRuleModal } from "../../modals";
|
||||
import type DynamicThemeBackgroundPlugin from "../../plugin";
|
||||
import type { DTBSettings, TimeRule } from "../../types";
|
||||
import { DragSort, addDropdownTooltip, addEnhancedDropdownTooltip, generateId } from "../../utils";
|
||||
|
||||
export class ModeSettingsSection {
|
||||
private plugin: DynamicThemeBackgroundPlugin;
|
||||
private defaultSettings: DTBSettings;
|
||||
private container!: HTMLElement;
|
||||
private timeRulesContainer: HTMLElement | null = null;
|
||||
private timeRuleDragSort: DragSort<TimeRule> | null = null;
|
||||
|
||||
constructor(plugin: DynamicThemeBackgroundPlugin, defaultSettings: DTBSettings) {
|
||||
this.plugin = plugin;
|
||||
this.defaultSettings = defaultSettings;
|
||||
}
|
||||
|
||||
display(container: HTMLElement): void {
|
||||
this.container = container;
|
||||
const containerEl = this.container;
|
||||
containerEl.empty();
|
||||
|
||||
// 模式设置标题
|
||||
new Setting(containerEl).setName(t("mode_settings_title")).setHeading();
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("switch_mode_name"))
|
||||
.setDesc(t("switch_mode_desc"))
|
||||
.addDropdown((dropdown) => {
|
||||
dropdown
|
||||
.addOption("time-based", t("mode_time_based"))
|
||||
.addOption("interval", t("mode_interval"))
|
||||
.addOption("manual", t("mode_manual"))
|
||||
.setValue(this.plugin.settings.mode)
|
||||
.onChange(async (value: string) => {
|
||||
const mode = value as DTBSettings["mode"];
|
||||
this.plugin.settings.mode = mode;
|
||||
await this.plugin.saveSettings();
|
||||
this.plugin.startBackgroundManager();
|
||||
this.display(this.container);
|
||||
});
|
||||
// 添加模式切换的 tooltip
|
||||
addDropdownTooltip(
|
||||
dropdown,
|
||||
{
|
||||
"time-based": t("mode_time_based_tooltip"),
|
||||
interval: t("mode_interval_tooltip"),
|
||||
manual: t("mode_manual_tooltip"),
|
||||
},
|
||||
t("switch_mode_desc")
|
||||
);
|
||||
|
||||
return dropdown;
|
||||
});
|
||||
|
||||
// 时间规则(仅在时间模式下显示)
|
||||
if (this.plugin.settings.mode === "time-based") {
|
||||
new Setting(containerEl).setName(t("time_rules_title")).setHeading();
|
||||
const buttonContainer = containerEl.createDiv("dtb-large-button-container");
|
||||
new Setting(buttonContainer)
|
||||
.setName(t("manage_time_rules_name"))
|
||||
.setDesc(t("manage_time_rules_desc"))
|
||||
.addButton((button) => {
|
||||
button.setButtonText(t("add_time_rule_button"));
|
||||
button.setTooltip(t("add_time_rule_tooltip"));
|
||||
button.onClick(() => this.showTimeRuleModal());
|
||||
})
|
||||
.addButton((button) => {
|
||||
button.setButtonText(t("clear_time_rules_button"));
|
||||
button.setTooltip(t("clear_time_rules_tooltip"));
|
||||
button.onClick(() => {
|
||||
new ConfirmModal(this.plugin.app, {
|
||||
message: t("confirm_clear_time_rules"),
|
||||
onConfirm: () => {
|
||||
this.plugin.settings.timeRules = [];
|
||||
this.plugin.startBackgroundManager(); // 重新启动背景管理器以应用更改
|
||||
void this.plugin.saveSettings();
|
||||
this.display(this.container);
|
||||
},
|
||||
}).open();
|
||||
});
|
||||
})
|
||||
.addButton((button) => {
|
||||
button.setButtonText(t("reset_time_rules_button"));
|
||||
button.setTooltip(t("reset_time_rules_tooltip"));
|
||||
button.onClick(() => {
|
||||
this.plugin.settings.timeRules = this.defaultSettings.timeRules;
|
||||
this.plugin.startBackgroundManager(); // 重新启动背景管理器以应用更改
|
||||
void this.plugin.saveSettings();
|
||||
this.display(this.container);
|
||||
});
|
||||
});
|
||||
|
||||
// 添加时间规则提示
|
||||
const hint = containerEl.createDiv("dtb-hint");
|
||||
hint.textContent = t("time_rule_hint");
|
||||
|
||||
// 显示时间规则列表
|
||||
this.timeRulesContainer = containerEl.createDiv("dtb-section-container");
|
||||
this.displayTimeRules();
|
||||
} else {
|
||||
this.timeRulesContainer = null;
|
||||
}
|
||||
|
||||
// 时间间隔设置(仅在间隔模式下显示)
|
||||
if (this.plugin.settings.mode === "interval") {
|
||||
new Setting(containerEl)
|
||||
.setName(t("interval_name"))
|
||||
.setDesc(t("interval_desc"))
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("60")
|
||||
.setValue(this.plugin.settings.intervalMinutes.toString())
|
||||
.onChange(async (value) => {
|
||||
const minutes = parseInt(value) || 60;
|
||||
this.plugin.settings.intervalMinutes = minutes;
|
||||
await this.plugin.saveSettings();
|
||||
this.plugin.startBackgroundManager();
|
||||
})
|
||||
);
|
||||
|
||||
// 随机壁纸设置
|
||||
new Setting(containerEl)
|
||||
.setName(t("enable_random_wallpaper_name"))
|
||||
.setDesc(t("enable_random_wallpaper_desc"))
|
||||
.addToggle((toggle) =>
|
||||
toggle.setValue(this.plugin.settings.enableRandomWallpaper).onChange(async (value) => {
|
||||
this.plugin.settings.enableRandomWallpaper = value;
|
||||
await this.plugin.saveSettings();
|
||||
this.display(this.container);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 显示时间规则列表,支持编辑、删除和添加新规则
|
||||
displayTimeRules(): void {
|
||||
const container = this.timeRulesContainer;
|
||||
if (!container) return;
|
||||
this.timeRuleDragSort?.disableAllDrag();
|
||||
container.empty();
|
||||
|
||||
// 初始化时间规则拖拽排序
|
||||
this.timeRuleDragSort = new DragSort<TimeRule>({
|
||||
container,
|
||||
items: this.plugin.settings.timeRules,
|
||||
getItemId: (rule) => rule.id,
|
||||
itemClass: "dtb-draggable",
|
||||
idDataAttribute: "ruleId",
|
||||
onReorder: async (reorderedRules) => {
|
||||
this.plugin.settings.timeRules = reorderedRules;
|
||||
await this.plugin.saveSettings();
|
||||
this.displayTimeRules();
|
||||
},
|
||||
});
|
||||
|
||||
// 获取当前激活的时间规则
|
||||
const activeRule = this.plugin.getCurrentTimeRule();
|
||||
|
||||
this.plugin.settings.timeRules.forEach((rule: TimeRule) => {
|
||||
const setting = new Setting(container);
|
||||
|
||||
setting.setName(rule.name).setDesc(`${rule.startTime} - ${rule.endTime}`);
|
||||
|
||||
// 如果是激活的时间规则,则添加一个提示图标
|
||||
if (rule.id === activeRule?.id) {
|
||||
const indicator = setting.controlEl.createDiv();
|
||||
indicator.setText("🔥");
|
||||
indicator.title = t("active_time_rule");
|
||||
}
|
||||
|
||||
setting
|
||||
.addToggle((toggle) =>
|
||||
toggle.setValue(rule.enabled).onChange(async (value) => {
|
||||
rule.enabled = value;
|
||||
this.plugin.startBackgroundManager(); // 重新启动背景管理器以应用更改
|
||||
await this.plugin.saveSettings();
|
||||
this.displayTimeRules();
|
||||
})
|
||||
)
|
||||
.addDropdown((dropdown) => {
|
||||
dropdown.addOption("", t("select_background_option"));
|
||||
this.plugin.settings.backgrounds.forEach((bg) => {
|
||||
dropdown.addOption(bg.id, bg.name);
|
||||
});
|
||||
// 使用增强版 tooltip 方法为背景选择下拉框添加动态提示
|
||||
addEnhancedDropdownTooltip(dropdown, {
|
||||
defaultTooltip: t("select_background_option"),
|
||||
showSelectedValue: true,
|
||||
customFormatter: (value, text) => {
|
||||
if (!value) return t("select_background_option");
|
||||
const bg = this.plugin.settings.backgrounds.find((b) => b.id === value);
|
||||
if (bg) {
|
||||
return `${text} (${bg.type.toUpperCase()})`;
|
||||
}
|
||||
return text;
|
||||
},
|
||||
});
|
||||
|
||||
return dropdown.setValue(rule.backgroundId).onChange((value) => {
|
||||
rule.backgroundId = value;
|
||||
void this.plugin.saveSettings();
|
||||
void this.plugin.updateBackground(true);
|
||||
});
|
||||
})
|
||||
.addButton((button) =>
|
||||
button.setButtonText(t("button_edit")).onClick(() => this.showTimeRuleModal(rule))
|
||||
)
|
||||
.addButton((button) =>
|
||||
button.setButtonText(t("button_delete")).onClick(() => {
|
||||
this.plugin.settings.timeRules = this.plugin.settings.timeRules.filter((r) => r.id !== rule.id);
|
||||
this.plugin.startBackgroundManager(); // 重新启动背景管理器以应用更改
|
||||
void this.plugin.saveSettings();
|
||||
this.displayTimeRules();
|
||||
})
|
||||
);
|
||||
|
||||
// 设置拖拽属性
|
||||
setting.settingEl.addClass("dtb-draggable");
|
||||
setting.settingEl.dataset.ruleId = rule.id;
|
||||
|
||||
// 添加通用条目样式类
|
||||
setting.settingEl.addClass("dtb-button-container"); // 按钮样式
|
||||
|
||||
// 启用拖拽功能
|
||||
this.timeRuleDragSort?.enableDragForElement(setting.settingEl, rule);
|
||||
});
|
||||
}
|
||||
|
||||
// 显示添加或编辑时间规则的模态窗口
|
||||
private showTimeRuleModal(rule?: TimeRule) {
|
||||
// 如果没有提供规则,创建一个新的空规则
|
||||
const editRule: TimeRule = rule ?? {
|
||||
id: "",
|
||||
name: "",
|
||||
startTime: "09:00",
|
||||
endTime: "17:00",
|
||||
backgroundId: "",
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
const modal = new TimeRuleModal(this.plugin.app, editRule, (updatedRule) => {
|
||||
if (!updatedRule.name.trim() || !updatedRule.startTime || !updatedRule.endTime) {
|
||||
new Notice(t("notice_all_fields_required"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (rule) {
|
||||
// 编辑现有规则
|
||||
const index = this.plugin.settings.timeRules.findIndex((r) => r.id === rule.id);
|
||||
if (index !== -1) {
|
||||
this.plugin.settings.timeRules[index] = {
|
||||
...rule,
|
||||
name: updatedRule.name.trim(),
|
||||
startTime: updatedRule.startTime,
|
||||
endTime: updatedRule.endTime,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
// 添加新规则
|
||||
const newRule: TimeRule = {
|
||||
id: generateId("rule"),
|
||||
name: updatedRule.name.trim(),
|
||||
startTime: updatedRule.startTime,
|
||||
endTime: updatedRule.endTime,
|
||||
backgroundId: this.plugin.settings.backgrounds[0]?.id ?? "",
|
||||
enabled: true,
|
||||
};
|
||||
this.plugin.settings.timeRules.push(newRule);
|
||||
}
|
||||
|
||||
this.plugin.startBackgroundManager(); // 重新启动背景管理器以应用更改
|
||||
void this.plugin.saveSettings();
|
||||
this.displayTimeRules();
|
||||
});
|
||||
|
||||
modal.open();
|
||||
}
|
||||
|
||||
cleanup(): void {
|
||||
this.timeRuleDragSort?.disableAllDrag();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -47,16 +47,16 @@ export class DTBSettingsView extends ItemView {
|
|||
}
|
||||
|
||||
async onClose(): Promise<void> {
|
||||
// 清理设置标签页的订阅
|
||||
if (this.settingTab) {
|
||||
this.settingTab.cleanup();
|
||||
}
|
||||
// hide() 调用 cleanup() + 设置 active=false + 从 settingTabs Map 中移除
|
||||
this.settingTab.hide();
|
||||
|
||||
// 清理资源
|
||||
if (this.settingTab && this.settingTab.containerEl) {
|
||||
this.settingTab.containerEl.empty();
|
||||
// 清理 DOM
|
||||
if (this.settingTab.containerEl) {
|
||||
this.settingTab.containerEl.empty();
|
||||
}
|
||||
}
|
||||
this.settingTab = null; // 释放引用,帮助垃圾回收
|
||||
this.settingTab = null;
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
* 支持对任意列表进行拖拽排序操作
|
||||
*/
|
||||
|
||||
import { logger } from "../core/logger";
|
||||
|
||||
/**
|
||||
* 拖拽排序配置接口
|
||||
*/
|
||||
|
|
@ -80,11 +82,10 @@ export class DragSort<T> {
|
|||
element.classList.remove(this.config.itemClass);
|
||||
}
|
||||
|
||||
// 移除事件监听器
|
||||
// 移除事件监听器(WeakMap 自动清理引用,无需手动 delete)
|
||||
const cleanup = this.dragHandles.get(element);
|
||||
if (cleanup) {
|
||||
cleanup();
|
||||
this.dragHandles.delete(element);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -205,7 +206,7 @@ export class DragSort<T> {
|
|||
const targetIndex = items.findIndex((item) => this.config.getItemId(item) === targetId);
|
||||
|
||||
if (draggedIndex === -1 || targetIndex === -1) {
|
||||
console.warn("DTB: Invalid drag operation - item not found");
|
||||
logger.warn("Invalid drag operation - item not found");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
* 简化设计:移除接口冗余,只保留必要的抽象类
|
||||
*/
|
||||
|
||||
import { logger } from "../../core/logger";
|
||||
import { generateId } from "../../utils/utils";
|
||||
import type {
|
||||
WallpaperApiConfig,
|
||||
WallpaperApiEndpoints,
|
||||
|
|
@ -66,12 +68,56 @@ export abstract class BaseWallpaperApi {
|
|||
}
|
||||
|
||||
// ============================================================================
|
||||
// 抽象方法 - 子类必须实现
|
||||
// 抽象/模板方法
|
||||
// ============================================================================
|
||||
|
||||
abstract init(): Promise<boolean>; // 启用插件时必须调用
|
||||
abstract deinit(): Promise<boolean>; // 禁用插件时必须调用
|
||||
abstract updateImageCache(): Promise<boolean>; // 更新图片缓存数据 wallpaperImageCache
|
||||
|
||||
/**
|
||||
* 禁用时清理(默认实现,子类可覆写)
|
||||
*/
|
||||
deinit(): Promise<boolean> {
|
||||
if (!this.initialized) return Promise.resolve(true);
|
||||
this.wallpaperImageCache = [];
|
||||
this.curDataIndex = 0;
|
||||
this.currentPage = 1;
|
||||
this.initialized = false;
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新图片缓存(模板方法)
|
||||
* 标准分页提供者只需覆写 fetchPage(),无需覆写此方法
|
||||
* 非标准分页提供者(如 qihoo360、custom)可直接覆写
|
||||
*/
|
||||
async updateImageCache(): Promise<boolean> {
|
||||
if (this.totalPages > 0 && this.currentPage > this.totalPages) {
|
||||
this.currentPage = 1;
|
||||
}
|
||||
const success = await this.fetchPage(this.currentPage);
|
||||
if (success) {
|
||||
this.currentPage += 1;
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定页的数据(子类实现)
|
||||
* 默认实现抛出错误,子类必须覆写 fetchPage 或 updateImageCache
|
||||
*/
|
||||
protected fetchPage(_page: number): Promise<boolean> {
|
||||
throw new Error("fetchPage must be implemented by subclass or override updateImageCache");
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化完成的公共收尾逻辑
|
||||
*/
|
||||
protected finishInit(): void {
|
||||
this.wallpaperImageCache = [];
|
||||
this.curDataIndex = 0;
|
||||
this.currentPage = 1;
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 静态方法 - 基类中抛出错误的方法,子类必须实现
|
||||
|
|
@ -169,7 +215,7 @@ export abstract class BaseWallpaperApi {
|
|||
}
|
||||
return success;
|
||||
} catch (error) {
|
||||
console.error(`Failed to enable API "${this.name}":`, error);
|
||||
logger.error(`Failed to enable API "${this.name}":`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -186,7 +232,7 @@ export abstract class BaseWallpaperApi {
|
|||
}
|
||||
return success;
|
||||
} catch (error) {
|
||||
console.error(`Failed to disable API "${this.name}":`, error);
|
||||
logger.error(`Failed to disable API "${this.name}":`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -196,13 +242,29 @@ export abstract class BaseWallpaperApi {
|
|||
// ============================================================================
|
||||
|
||||
generateBackgroundId(): string {
|
||||
return `${this.getId()}-${Date.now()}`;
|
||||
return generateId(this.getId());
|
||||
}
|
||||
|
||||
generateBackgroundName(): string {
|
||||
return `${this.getName()} - ${new Date().toLocaleString()}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全地将未知值转换为字符串
|
||||
*/
|
||||
protected safeString(value: unknown): string {
|
||||
if (value === null || value === undefined) {
|
||||
return "";
|
||||
}
|
||||
if (typeof value === "string") {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === "number" || typeof value === "boolean") {
|
||||
return String(value);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
protected saveConfig(): void {
|
||||
// 将 api 的配置同步过到 config 中
|
||||
this.config.baseUrl = this.baseUrl;
|
||||
|
|
@ -282,9 +344,9 @@ export abstract class BaseWallpaperApi {
|
|||
}
|
||||
|
||||
if (images.length < imageNum) {
|
||||
await this.updateImageCache();
|
||||
if (this.wallpaperImageCache.length === 0) {
|
||||
break; // 没有更多图片了
|
||||
const updated = await this.updateImageCache();
|
||||
if (!updated || this.wallpaperImageCache.length === 0) {
|
||||
break; // 获取失败或没有更多图片
|
||||
}
|
||||
this.curDataIndex = 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@
|
|||
|
||||
import { JSONPath } from "jsonpath-plus";
|
||||
import { requestUrl } from "obsidian";
|
||||
import { logger } from "../../core/logger";
|
||||
import { generateId } from "../../utils/utils";
|
||||
import {
|
||||
apiRegistry,
|
||||
BaseWallpaperApi,
|
||||
|
|
@ -71,13 +73,13 @@ export class CustomApi extends BaseWallpaperApi {
|
|||
|
||||
validateParams(_params: WallpaperApiParams): boolean {
|
||||
if (!this.baseUrl) {
|
||||
console.warn("Custom API: baseUrl is required");
|
||||
logger.warn("Custom API: baseUrl is required");
|
||||
return false;
|
||||
}
|
||||
|
||||
const urlJsonPath = this.config.customSettings?.imageUrlJsonPath;
|
||||
if (!urlJsonPath) {
|
||||
console.warn("Custom API: imageUrlJsonPath is required");
|
||||
logger.warn("Custom API: imageUrlJsonPath is required");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -97,37 +99,24 @@ export class CustomApi extends BaseWallpaperApi {
|
|||
// 测试连通性
|
||||
const images = await this.fetchImages();
|
||||
if (!images || images.length === 0) {
|
||||
console.warn("Custom API initialization failed: No images returned.");
|
||||
logger.warn("Custom API initialization failed: No images returned.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 初始化数据缓存
|
||||
this.wallpaperImageCache = [];
|
||||
this.curDataIndex = 0;
|
||||
this.currentPage = 1;
|
||||
this.finishInit();
|
||||
this.totalPages = 1; // Custom API 通常只有一页
|
||||
this.totalCount = images.length;
|
||||
|
||||
this.initialized = true;
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Custom API initialization failed:", error);
|
||||
logger.error("Custom API initialization failed:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
deinit(): Promise<boolean> {
|
||||
if (!this.initialized) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
// 清理缓存数据
|
||||
this.wallpaperImageCache = [];
|
||||
this.curDataIndex = 0;
|
||||
this.currentPage = 1;
|
||||
|
||||
this.initialized = false;
|
||||
return Promise.resolve(true);
|
||||
return super.deinit(); // 重置 cache, curDataIndex, currentPage, initialized
|
||||
}
|
||||
|
||||
async updateImageCache(): Promise<boolean> {
|
||||
|
|
@ -141,7 +130,7 @@ export class CustomApi extends BaseWallpaperApi {
|
|||
}
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error("Custom API cache update failed:", error);
|
||||
logger.error("Custom API cache update failed:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -169,7 +158,7 @@ export class CustomApi extends BaseWallpaperApi {
|
|||
const data = response.json;
|
||||
return this.transformCustomResponse(data);
|
||||
} catch (error) {
|
||||
console.error("Custom API fetch error:", error);
|
||||
logger.error("Custom API fetch error:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
|
@ -184,7 +173,7 @@ export class CustomApi extends BaseWallpaperApi {
|
|||
const urlJsonPath = this.config.customSettings?.imageUrlJsonPath;
|
||||
|
||||
if (!urlJsonPath) {
|
||||
console.warn("Custom API: imageUrlJsonPath is required for JSON response");
|
||||
logger.warn("Custom API: imageUrlJsonPath is required for JSON response");
|
||||
return [];
|
||||
}
|
||||
|
||||
|
|
@ -196,7 +185,7 @@ export class CustomApi extends BaseWallpaperApi {
|
|||
});
|
||||
|
||||
if (!urls || (Array.isArray(urls) && urls.length === 0)) {
|
||||
console.warn("Custom API: No URLs found at path:", urlJsonPath);
|
||||
logger.warn("Custom API: No URLs found at path:", urlJsonPath);
|
||||
return [];
|
||||
}
|
||||
|
||||
|
|
@ -213,15 +202,14 @@ export class CustomApi extends BaseWallpaperApi {
|
|||
};
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn("Error parsing custom API response:", error);
|
||||
logger.warn("Error parsing custom API response:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// 生成图片ID
|
||||
private generateImageId(index?: number): string {
|
||||
const timestamp = Date.now();
|
||||
return `custom_${timestamp}_${index !== undefined ? index : Math.random().toString(36).substring(2, 9)}`;
|
||||
private generateImageId(_index?: number): string {
|
||||
return generateId("custom");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
*/
|
||||
|
||||
import { Notice, requestUrl } from "obsidian";
|
||||
import { logger } from "../../core/logger";
|
||||
import { t } from "../../i18n";
|
||||
import {
|
||||
apiRegistry,
|
||||
|
|
@ -221,14 +222,14 @@ export class PexelsApi extends BaseWallpaperApi {
|
|||
}
|
||||
|
||||
if (!response || !response.photos || !Array.isArray(response.photos)) {
|
||||
console.warn("Pexels API initialization failed: Invalid response format.");
|
||||
logger.warn("Pexels API initialization failed: Invalid response format.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 更新分页信息
|
||||
// 更新分页信息(先更新 perPage,再计算 totalPages)
|
||||
this.perPage = Number(this.params.per_page) || 15;
|
||||
this.totalPages = response.total_results ? Math.ceil(response.total_results / this.perPage) : -1;
|
||||
this.totalCount = response.total_results ?? -1;
|
||||
this.perPage = Number(this.params.per_page) || 15;
|
||||
|
||||
new Notice(
|
||||
t("api_initialized_notice", {
|
||||
|
|
@ -238,65 +239,34 @@ export class PexelsApi extends BaseWallpaperApi {
|
|||
})
|
||||
);
|
||||
|
||||
// 初始化数据缓存
|
||||
this.wallpaperImageCache = [];
|
||||
this.curDataIndex = 0;
|
||||
this.currentPage = 1;
|
||||
|
||||
this.initialized = true;
|
||||
this.finishInit();
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.warn("Pexels API initialization failed:", error);
|
||||
logger.warn("Pexels API initialization failed:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
deinit(): Promise<boolean> {
|
||||
if (!this.initialized) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
// 清理缓存数据
|
||||
this.wallpaperImageCache = [];
|
||||
this.curDataIndex = 0;
|
||||
this.currentPage = 1;
|
||||
|
||||
this.initialized = false;
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
async updateImageCache(): Promise<boolean> {
|
||||
// 如果已经到最后一页了,跳到第一页
|
||||
if (this.totalPages > 0 && this.currentPage > this.totalPages) {
|
||||
this.currentPage = 1;
|
||||
}
|
||||
|
||||
let success = false;
|
||||
if (this.params.query) {
|
||||
// 有搜索查询,使用搜索接口
|
||||
success = await this.fetchAndCacheSearchResults(this.currentPage);
|
||||
} else {
|
||||
// 没有搜索查询,使用精选接口
|
||||
success = await this.fetchAndCacheCuratedPhotos(this.currentPage);
|
||||
}
|
||||
|
||||
if (success) {
|
||||
this.currentPage += 1;
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 辅助方法
|
||||
// ============================================================================
|
||||
|
||||
protected async fetchPage(page: number): Promise<boolean> {
|
||||
const query = this.params.query as string | undefined;
|
||||
if (query && query.trim() !== "") {
|
||||
return this.fetchAndCacheSearchResults(page);
|
||||
} else {
|
||||
return this.fetchAndCacheCuratedPhotos(page);
|
||||
}
|
||||
}
|
||||
|
||||
// 拉取搜索结果并缓存
|
||||
private async fetchAndCacheSearchResults(page = this.currentPage): Promise<boolean> {
|
||||
try {
|
||||
const data = await this.fetchSearchResults(page);
|
||||
|
||||
if (!data || !data.photos || !Array.isArray(data.photos)) {
|
||||
console.warn("Invalid search response format from Pexels API");
|
||||
logger.warn("Invalid search response format from Pexels API");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -310,7 +280,7 @@ export class PexelsApi extends BaseWallpaperApi {
|
|||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.warn(`Error fetching search results from Pexels API:`, error);
|
||||
logger.warn("Error fetching search results from Pexels API:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -320,8 +290,9 @@ export class PexelsApi extends BaseWallpaperApi {
|
|||
try {
|
||||
const data = await this.fetchCuratedPhotos(page);
|
||||
|
||||
if (!data || !data.photos || !Array.isArray(data.photos)) {
|
||||
console.warn("Invalid curated photos response format from Pexels API");
|
||||
if (!data || !data.photos || !Array.isArray(data.photos) || data.photos.length === 0) {
|
||||
logger.warn("Invalid or empty curated photos response from Pexels API");
|
||||
this.currentPage = 1; // 重置页码以便下次从头获取
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -331,7 +302,7 @@ export class PexelsApi extends BaseWallpaperApi {
|
|||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.warn(`Error fetching curated photos from Pexels API:`, error);
|
||||
logger.warn("Error fetching curated photos from Pexels API:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -377,7 +348,7 @@ export class PexelsApi extends BaseWallpaperApi {
|
|||
queryParams.append("per_page", String(this.params.per_page || this.perPage));
|
||||
|
||||
const url = `${this.buildEndpointUrl("curated")}?${queryParams.toString()}`;
|
||||
console.debug(`Fetching Pixabay curated photos from: ${url}`);
|
||||
logger.debug(`Fetching Pexels curated photos from: ${url}`);
|
||||
const response = await requestUrl({
|
||||
url,
|
||||
headers: {
|
||||
|
|
@ -387,20 +358,6 @@ export class PexelsApi extends BaseWallpaperApi {
|
|||
return response.json;
|
||||
}
|
||||
|
||||
// 安全地将未知值转换为字符串,避免对象被转换为 [object Object]
|
||||
private safeString(value: unknown): string {
|
||||
if (value === null || value === undefined) {
|
||||
return "";
|
||||
}
|
||||
if (typeof value === "string") {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === "number" || typeof value === "boolean") {
|
||||
return String(value);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
// 辅助方法:转换 API 返回的图片数据为 WallpaperImage
|
||||
private transformPhoto(photo: Record<string, unknown>): WallpaperImage {
|
||||
const src = (photo.src as Record<string, unknown>) ?? {};
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
*/
|
||||
|
||||
import { Notice, requestUrl } from "obsidian";
|
||||
import { logger } from "../../core/logger";
|
||||
import { t } from "../../i18n";
|
||||
import {
|
||||
apiRegistry,
|
||||
|
|
@ -303,14 +304,14 @@ export class PixabayApi extends BaseWallpaperApi {
|
|||
const response = await this.fetchSearchResults(1);
|
||||
|
||||
if (!response || !response.hits || !Array.isArray(response.hits)) {
|
||||
console.warn("Pixabay API initialization failed: Invalid response format.");
|
||||
logger.warn("Pixabay API initialization failed: Invalid response format.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 更新分页信息
|
||||
// 更新分页信息(先更新 perPage,再计算 totalPages)
|
||||
this.perPage = Number(this.params.per_page) || 20;
|
||||
this.totalPages = response.totalHits ? Math.ceil(response.totalHits / this.perPage) : -1;
|
||||
this.totalCount = response.total ?? -1;
|
||||
this.perPage = Number(this.params.per_page) || 20;
|
||||
|
||||
new Notice(
|
||||
t("api_initialized_notice", {
|
||||
|
|
@ -320,57 +321,24 @@ export class PixabayApi extends BaseWallpaperApi {
|
|||
})
|
||||
);
|
||||
|
||||
// 初始化数据缓存
|
||||
this.wallpaperImageCache = [];
|
||||
this.curDataIndex = 0;
|
||||
this.currentPage = 1;
|
||||
|
||||
this.initialized = true;
|
||||
this.finishInit();
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.warn("Pixabay API initialization failed:", error);
|
||||
logger.warn("Pixabay API initialization failed:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
deinit(): Promise<boolean> {
|
||||
if (!this.initialized) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
// 清理缓存数据
|
||||
this.wallpaperImageCache = [];
|
||||
this.curDataIndex = 0;
|
||||
this.currentPage = 1;
|
||||
|
||||
this.initialized = false;
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
async updateImageCache(): Promise<boolean> {
|
||||
// 如果已经到最后一页了,跳到第一页
|
||||
if (this.totalPages > 0 && this.currentPage > this.totalPages) {
|
||||
this.currentPage = 1;
|
||||
}
|
||||
|
||||
const success = await this.fetchAndCacheSearchResults(this.currentPage);
|
||||
if (success) {
|
||||
this.currentPage += 1;
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 辅助方法
|
||||
// ============================================================================
|
||||
|
||||
// 拉取搜索结果并缓存
|
||||
private async fetchAndCacheSearchResults(page = this.currentPage): Promise<boolean> {
|
||||
protected async fetchPage(page: number): Promise<boolean> {
|
||||
try {
|
||||
const data = await this.fetchSearchResults(page);
|
||||
|
||||
if (!data || !data.hits || !Array.isArray(data.hits)) {
|
||||
console.warn("Invalid search response format from Pixabay API");
|
||||
logger.warn("Invalid search response format from Pixabay API");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -384,7 +352,7 @@ export class PixabayApi extends BaseWallpaperApi {
|
|||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.warn(`Error fetching search results from Pixabay API:`, error);
|
||||
logger.warn("Error fetching search results from Pixabay API:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -434,25 +402,11 @@ export class PixabayApi extends BaseWallpaperApi {
|
|||
}
|
||||
|
||||
const url = `${this.buildEndpointUrl("search")}?${queryParams.toString()}`;
|
||||
console.debug(`Fetching Pixabay search results from: ${url}`);
|
||||
logger.debug(`Fetching Pixabay search results from: ${url}`);
|
||||
const response = await requestUrl({ url });
|
||||
return response.json;
|
||||
}
|
||||
|
||||
// 安全地将未知值转换为字符串,避免对象被转换为 [object Object]
|
||||
private safeString(value: unknown): string {
|
||||
if (value === null || value === undefined) {
|
||||
return "";
|
||||
}
|
||||
if (typeof value === "string") {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === "number" || typeof value === "boolean") {
|
||||
return String(value);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
// 辅助方法:转换 API 返回的图片数据为 WallpaperImage
|
||||
private transformImage(image: Record<string, unknown>): WallpaperImage {
|
||||
const tagsStr = this.safeString(image.tags);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
*/
|
||||
|
||||
import { Notice, requestUrl } from "obsidian";
|
||||
import { logger } from "../../core/logger";
|
||||
import { generateId } from "../../utils/utils";
|
||||
import { t } from "../../i18n";
|
||||
import {
|
||||
apiRegistry,
|
||||
|
|
@ -254,20 +256,19 @@ export class Qihoo360Api extends BaseWallpaperApi {
|
|||
// 初始化时加载分类数据
|
||||
await this.loadCategories();
|
||||
await this.loadHotSearch();
|
||||
this.initialized = true;
|
||||
this.finishInit();
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("360壁纸API初始化失败:", error);
|
||||
new Notice(t("notice_api_failed_enable_disable", { action: "启用", apiName: this.name }));
|
||||
logger.error("360 API initialization failed:", error);
|
||||
new Notice(t("notice_api_failed_enable_disable", { action: t("action_enable"), apiName: this.name }));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
deinit(): Promise<boolean> {
|
||||
this.initialized = false;
|
||||
this.categoriesCache = [];
|
||||
this.hotSearchCache = [];
|
||||
return Promise.resolve(true);
|
||||
return super.deinit(); // 重置 cache, curDataIndex, currentPage, initialized
|
||||
}
|
||||
|
||||
async updateImageCache(): Promise<boolean> {
|
||||
|
|
@ -289,11 +290,11 @@ export class Qihoo360Api extends BaseWallpaperApi {
|
|||
this.curDataIndex = 0;
|
||||
return true;
|
||||
} else {
|
||||
console.warn("360壁纸API: 未获取到壁纸数据");
|
||||
logger.warn("360壁纸API: 未获取到壁纸数据");
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("360壁纸API更新缓存失败:", error);
|
||||
logger.error("360壁纸API更新缓存失败:", error);
|
||||
new Notice(t("notice_api_failed_fetch", { apiName: this.name }));
|
||||
return false;
|
||||
}
|
||||
|
|
@ -309,7 +310,7 @@ export class Qihoo360Api extends BaseWallpaperApi {
|
|||
private async loadCategories(): Promise<void> {
|
||||
try {
|
||||
const url = this.buildEndpointUrl("categories");
|
||||
console.debug("360壁纸API: 获取分类列表", url);
|
||||
logger.debug("360壁纸API: 获取分类列表", url);
|
||||
|
||||
const response = await requestUrl({
|
||||
url: url,
|
||||
|
|
@ -320,12 +321,12 @@ export class Qihoo360Api extends BaseWallpaperApi {
|
|||
|
||||
if (data.errno === "0" && data.data) {
|
||||
this.categoriesCache = data.data;
|
||||
console.debug(`360壁纸API: 成功加载${data.data.length}个分类`);
|
||||
logger.debug(`360壁纸API: 成功加载${data.data.length}个分类`);
|
||||
} else {
|
||||
throw new Error(`API返回错误: ${data.errmsg}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("360壁纸API: 加载分类失败", error);
|
||||
logger.error("360壁纸API: 加载分类失败", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
|
@ -340,7 +341,7 @@ export class Qihoo360Api extends BaseWallpaperApi {
|
|||
if (!url) {
|
||||
throw new Error("hotSearch endpoint not configured");
|
||||
}
|
||||
console.debug("360壁纸API: 获取热门搜索", url);
|
||||
logger.debug("360壁纸API: 获取热门搜索", url);
|
||||
|
||||
const response = await requestUrl({
|
||||
url: url,
|
||||
|
|
@ -351,10 +352,10 @@ export class Qihoo360Api extends BaseWallpaperApi {
|
|||
|
||||
if (data.error === 0 && data.data) {
|
||||
this.hotSearchCache = data.data;
|
||||
console.debug(`360壁纸API: 成功加载${data.data.length}个热门搜索词`);
|
||||
logger.debug(`360壁纸API: 成功加载${data.data.length}个热门搜索词`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("360壁纸API: 加载热门搜索失败", error);
|
||||
logger.error("360壁纸API: 加载热门搜索失败", error);
|
||||
// 热门搜索加载失败不影响主要功能
|
||||
}
|
||||
}
|
||||
|
|
@ -373,7 +374,7 @@ export class Qihoo360Api extends BaseWallpaperApi {
|
|||
});
|
||||
|
||||
const url = `${this.baseUrl}/index.php?${params.toString()}`;
|
||||
console.debug("360壁纸API: 获取最新壁纸", url);
|
||||
logger.debug("360壁纸API: 获取最新壁纸", url);
|
||||
|
||||
const response = await requestUrl({
|
||||
url: url,
|
||||
|
|
@ -403,7 +404,7 @@ export class Qihoo360Api extends BaseWallpaperApi {
|
|||
});
|
||||
|
||||
const url = `${this.baseUrl}/index.php?${params.toString()}`;
|
||||
console.debug("360壁纸API: 获取分类壁纸", url);
|
||||
logger.debug("360壁纸API: 获取分类壁纸", url);
|
||||
|
||||
const response = await requestUrl({
|
||||
url: url,
|
||||
|
|
@ -438,7 +439,7 @@ export class Qihoo360Api extends BaseWallpaperApi {
|
|||
});
|
||||
|
||||
const url = `${this.baseUrl}/index.php?${params.toString()}`;
|
||||
console.debug("360壁纸API: 搜索壁纸", url);
|
||||
logger.debug("360壁纸API: 搜索壁纸", url);
|
||||
|
||||
const response = await requestUrl({
|
||||
url: url,
|
||||
|
|
@ -457,7 +458,7 @@ export class Qihoo360Api extends BaseWallpaperApi {
|
|||
/**
|
||||
* 转换API响应为WallpaperImage格式
|
||||
*/
|
||||
private convertToWallpaperImage(item: Qihoo360WallpaperResponse["data"][number], index: number): WallpaperImage {
|
||||
private convertToWallpaperImage(item: Qihoo360WallpaperResponse["data"][number], _index: number): WallpaperImage {
|
||||
// 获取首选分辨率设置
|
||||
const preferredResolution = this.params.preferredResolution || "auto";
|
||||
let imageUrl = item.url;
|
||||
|
|
@ -516,7 +517,7 @@ export class Qihoo360Api extends BaseWallpaperApi {
|
|||
const categoryName = category ? category.name : `分类${item.cid}`;
|
||||
|
||||
return {
|
||||
id: item.pid || item.id || `360-${Date.now()}-${index}`,
|
||||
id: item.pid || item.id || generateId("360"),
|
||||
url: imageUrl,
|
||||
author: "360壁纸",
|
||||
description: tags.length > 0 ? tags.join(", ") : categoryName,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
*/
|
||||
|
||||
import { Notice, requestUrl } from "obsidian";
|
||||
import { logger } from "../../core/logger";
|
||||
import { t } from "../../i18n";
|
||||
import {
|
||||
apiRegistry,
|
||||
|
|
@ -204,7 +205,7 @@ export class UnsplashApi extends BaseWallpaperApi {
|
|||
// 测试API连通性 - 获取随机图片
|
||||
const response = await this.fetchRandomPhotos(1);
|
||||
if (!response || !Array.isArray(response) || response.length === 0) {
|
||||
console.warn("Unsplash API initialization failed: No response from random endpoint.");
|
||||
logger.warn("Unsplash API initialization failed: No response from random endpoint.");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -212,7 +213,7 @@ export class UnsplashApi extends BaseWallpaperApi {
|
|||
if (this.params.query) {
|
||||
const searchResp = await this.fetchSearchResults(1);
|
||||
if (!searchResp) {
|
||||
console.warn("Unsplash API initialization failed: No response from search endpoint.");
|
||||
logger.warn("Unsplash API initialization failed: No response from search endpoint.");
|
||||
return false;
|
||||
}
|
||||
this.totalPages = searchResp.total_pages ?? -1;
|
||||
|
|
@ -233,62 +234,34 @@ export class UnsplashApi extends BaseWallpaperApi {
|
|||
|
||||
this.perPage = Number(this.params.per_page) || 10;
|
||||
|
||||
// 初始化数据缓存
|
||||
this.wallpaperImageCache = [];
|
||||
this.curDataIndex = 0;
|
||||
this.currentPage = 1;
|
||||
|
||||
this.initialized = true;
|
||||
this.finishInit();
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.warn("Unsplash API initialization failed:", error);
|
||||
logger.warn("Unsplash API initialization failed:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
deinit(): Promise<boolean> {
|
||||
if (!this.initialized) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
// 清理缓存数据
|
||||
this.wallpaperImageCache = [];
|
||||
this.curDataIndex = 0;
|
||||
this.currentPage = 1;
|
||||
|
||||
this.initialized = false;
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
async updateImageCache(): Promise<boolean> {
|
||||
// 如果有搜索查询,使用搜索接口
|
||||
if (this.params.query) {
|
||||
// 如果已经到最后一页了,跳到第一页
|
||||
if (this.totalPages > 0 && this.currentPage > this.totalPages) {
|
||||
this.currentPage = 1;
|
||||
}
|
||||
const success = await this.fetchAndCacheSearchResults(this.currentPage);
|
||||
if (success) {
|
||||
this.currentPage += 1;
|
||||
}
|
||||
return success;
|
||||
} else {
|
||||
// 没有搜索查询,使用随机图片接口
|
||||
return await this.fetchAndCacheRandomPhotos();
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 辅助方法
|
||||
// ============================================================================
|
||||
|
||||
protected async fetchPage(page: number): Promise<boolean> {
|
||||
const query = this.params.query as string | undefined;
|
||||
if (query && query.trim() !== "") {
|
||||
return this.fetchAndCacheSearchResults(page);
|
||||
} else {
|
||||
return this.fetchAndCacheRandomPhotos();
|
||||
}
|
||||
}
|
||||
|
||||
// 拉取搜索结果并缓存
|
||||
private async fetchAndCacheSearchResults(page = this.currentPage): Promise<boolean> {
|
||||
try {
|
||||
const data = await this.fetchSearchResults(page);
|
||||
|
||||
if (!data || !data.results || !Array.isArray(data.results)) {
|
||||
console.warn("Invalid search response format from Unsplash API");
|
||||
logger.warn("Invalid search response format from Unsplash API");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -302,7 +275,7 @@ export class UnsplashApi extends BaseWallpaperApi {
|
|||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.warn(`Error fetching search results from Unsplash API:`, error);
|
||||
logger.warn("Error fetching search results from Unsplash API:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -313,7 +286,7 @@ export class UnsplashApi extends BaseWallpaperApi {
|
|||
const photos = await this.fetchRandomPhotos(this.perPage);
|
||||
|
||||
if (!photos || !Array.isArray(photos)) {
|
||||
console.warn("Invalid random photos response format from Unsplash API");
|
||||
logger.warn("Invalid random photos response format from Unsplash API");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -323,7 +296,7 @@ export class UnsplashApi extends BaseWallpaperApi {
|
|||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.warn(`Error fetching random photos from Unsplash API:`, error);
|
||||
logger.warn("Error fetching random photos from Unsplash API:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -356,7 +329,7 @@ export class UnsplashApi extends BaseWallpaperApi {
|
|||
}
|
||||
|
||||
const url = `${this.buildEndpointUrl("search")}?${queryParams.toString()}`;
|
||||
console.debug(`Fetching Unsplash search results from: ${url}`);
|
||||
logger.debug(`Fetching Unsplash search results from: ${url}`);
|
||||
const response = await requestUrl({ url });
|
||||
return response.json;
|
||||
}
|
||||
|
|
@ -384,25 +357,11 @@ export class UnsplashApi extends BaseWallpaperApi {
|
|||
}
|
||||
|
||||
const url = `${this.buildEndpointUrl("random")}?${queryParams.toString()}`;
|
||||
console.debug(`Fetching Unsplash random photos from: ${url}`);
|
||||
logger.debug(`Fetching Unsplash random photos from: ${url}`);
|
||||
const response = await requestUrl({ url });
|
||||
return response.json;
|
||||
}
|
||||
|
||||
// 安全地将未知值转换为字符串,避免对象被转换为 [object Object]
|
||||
private safeString(value: unknown): string {
|
||||
if (value === null || value === undefined) {
|
||||
return "";
|
||||
}
|
||||
if (typeof value === "string") {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === "number" || typeof value === "boolean") {
|
||||
return String(value);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
// 辅助方法:转换 API 返回的图片数据为 WallpaperImage
|
||||
private transformPhoto(photo: Record<string, unknown>): WallpaperImage {
|
||||
const user = (photo.user as Record<string, unknown>) ?? {};
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
*/
|
||||
|
||||
import { Notice, requestUrl } from "obsidian";
|
||||
import { logger } from "../../core/logger";
|
||||
import { t } from "../../i18n";
|
||||
import {
|
||||
apiRegistry,
|
||||
|
|
@ -89,14 +90,15 @@ export class WallhavenApi extends BaseWallpaperApi {
|
|||
{ value: "anime", label: "Anime" },
|
||||
{ value: "people", label: "People" },
|
||||
],
|
||||
toApiValue: (uiValue: string[]) => {
|
||||
toApiValue: (uiValue) => {
|
||||
const arr = Array.isArray(uiValue) ? uiValue : [String(uiValue)];
|
||||
let result = "";
|
||||
result += uiValue.includes("general") ? "1" : "0";
|
||||
result += uiValue.includes("anime") ? "1" : "0";
|
||||
result += uiValue.includes("people") ? "1" : "0";
|
||||
return result ?? (defaultParams.categories as string);
|
||||
result += arr.includes("general") ? "1" : "0";
|
||||
result += arr.includes("anime") ? "1" : "0";
|
||||
result += arr.includes("people") ? "1" : "0";
|
||||
return result || (defaultParams.categories as string);
|
||||
},
|
||||
fromApiValue: (apiValue: string) => {
|
||||
fromApiValue: (apiValue) => {
|
||||
const str = apiValue?.toString() ?? (defaultParams.categories as string);
|
||||
const result: string[] = [];
|
||||
if (str[0] === "1") result.push("general");
|
||||
|
|
@ -116,15 +118,15 @@ export class WallhavenApi extends BaseWallpaperApi {
|
|||
{ value: "sketchy", label: "Sketchy" },
|
||||
{ value: "nsfw", label: "NSFW (18+)" },
|
||||
],
|
||||
toApiValue: (uiValue: string[]) => {
|
||||
const arr = Array.isArray(uiValue) ? uiValue : [];
|
||||
toApiValue: (uiValue) => {
|
||||
const arr = Array.isArray(uiValue) ? uiValue : [String(uiValue)];
|
||||
let result = "";
|
||||
result += arr.includes("sfw") ? "1" : "0";
|
||||
result += arr.includes("sketchy") ? "1" : "0";
|
||||
result += arr.includes("nsfw") ? "1" : "0";
|
||||
return result || (defaultParams.purity as string);
|
||||
},
|
||||
fromApiValue: (apiValue: string) => {
|
||||
fromApiValue: (apiValue) => {
|
||||
const str = apiValue?.toString() ?? (defaultParams.purity as string);
|
||||
const result: string[] = [];
|
||||
if (str[0] === "1") result.push("sfw");
|
||||
|
|
@ -291,21 +293,21 @@ export class WallhavenApi extends BaseWallpaperApi {
|
|||
// 拉取第一页数据用于测试连通性
|
||||
const resp = await this.fetchSearchResults(1);
|
||||
if (!resp) {
|
||||
console.warn("Wallhaven API initialization failed: No response from search endpoint.");
|
||||
logger.warn("Wallhaven API initialization failed: No response from search endpoint.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 填充 totalPages、totalCount 等状态
|
||||
const meta = resp.meta;
|
||||
if (!meta) {
|
||||
console.warn("Wallhaven API response missing meta information.");
|
||||
logger.warn("Wallhaven API response missing meta information.");
|
||||
return false;
|
||||
}
|
||||
this.totalPages = meta.last_page ?? -1;
|
||||
this.totalCount = meta.total ?? -1;
|
||||
this.perPage = meta.per_page ?? -1;
|
||||
if (this.totalPages <= 0 || this.totalCount < 0 || this.perPage <= 0) {
|
||||
console.warn("Wallhaven API response has invalid pagination data.");
|
||||
logger.warn("Wallhaven API response has invalid pagination data.");
|
||||
return false;
|
||||
}
|
||||
new Notice(
|
||||
|
|
@ -316,59 +318,28 @@ export class WallhavenApi extends BaseWallpaperApi {
|
|||
})
|
||||
);
|
||||
|
||||
// 初始化数据缓存
|
||||
this.wallpaperImageCache = [];
|
||||
this.curDataIndex = 0;
|
||||
this.currentPage = 1;
|
||||
|
||||
this.initialized = true;
|
||||
this.finishInit();
|
||||
return true;
|
||||
}
|
||||
|
||||
deinit(): Promise<boolean> {
|
||||
if (!this.initialized) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
// 清理缓存数据
|
||||
this.wallpaperImageCache = [];
|
||||
this.curDataIndex = 0;
|
||||
this.currentPage = 1;
|
||||
|
||||
this.initialized = false;
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
async updateImageCache(): Promise<boolean> {
|
||||
// 如果已经到最后一页了,跳到第一页
|
||||
if (this.totalPages > 0 && this.currentPage > this.totalPages) {
|
||||
this.currentPage = 1;
|
||||
}
|
||||
const success = await this.fetchAndCachePageImages(this.currentPage);
|
||||
if (success) {
|
||||
this.currentPage += 1;
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 辅助方法
|
||||
// ============================================================================
|
||||
|
||||
// 拉取指定页码的壁纸数据并缓存到 wallpaperImageCache
|
||||
private async fetchAndCachePageImages(page = this.currentPage): Promise<boolean> {
|
||||
protected async fetchPage(page: number): Promise<boolean> {
|
||||
try {
|
||||
const data = await this.fetchSearchResults(page);
|
||||
|
||||
if (!data.data || !Array.isArray(data.data)) {
|
||||
console.warn("Invalid API response format from Wallhaven");
|
||||
logger.warn("Invalid API response format from Wallhaven");
|
||||
return false;
|
||||
}
|
||||
// 为避免爆内存,这里应仅缓存当前页的数据
|
||||
this.wallpaperImageCache = data.data.map((img: Record<string, unknown>) => this.transformImage(img));
|
||||
this.curDataIndex = 0;
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.warn(`Error fetching images from Wallhaven API:`, error);
|
||||
logger.warn("Error fetching images from Wallhaven API:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -378,25 +349,11 @@ export class WallhavenApi extends BaseWallpaperApi {
|
|||
// 合并参数并使用基类方法构建查询字符串
|
||||
const allParams = { ...this.params, page };
|
||||
const url = `${this.buildEndpointUrl("search")}?${this.buildUrlParams(allParams)}`;
|
||||
console.debug(`Fetching Wallhaven search results from: ${url}`);
|
||||
logger.debug(`Fetching Wallhaven search results from: ${url}`);
|
||||
const response = await requestUrl({ url });
|
||||
return response.json;
|
||||
}
|
||||
|
||||
// 安全地将未知值转换为字符串,避免对象被转换为 [object Object]
|
||||
private safeString(value: unknown): string {
|
||||
if (value === null || value === undefined) {
|
||||
return "";
|
||||
}
|
||||
if (typeof value === "string") {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === "number" || typeof value === "boolean") {
|
||||
return String(value);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
// 辅助方法:转换 API 返回的图片数据为 WallpaperImage
|
||||
private transformImage(apiImage: Record<string, unknown>): WallpaperImage {
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in a new issue