feat: Major Update: Added wallpaper API support; integrated Wallhaven API; optimized project structure and several other features.

重磅更新:支持壁纸API功能;添加对 wallhaven 的 API 支持; 项目结构和其他若干功能优化

BREAKING CHANGE:
This commit is contained in:
sean2077 2025-08-06 03:22:59 +08:00
parent c6f1d4675f
commit 8c65861080
41 changed files with 5565 additions and 2508 deletions

View file

@ -2,6 +2,7 @@
"trailingComma": "es5",
"tabWidth": 4,
"semi": true,
"printWidth": 120,
"plugins": [],
"overrides": []
}
}

View file

@ -10,6 +10,7 @@ This is a plugin for Obsidian that dynamically changes backgrounds based on time
- **Time-based backgrounds**: Automatically switch backgrounds according to time periods
- **Manual switching**: Allows users to manually switch backgrounds
- **🆕 Random Wallpaper Support**: Fetch random wallpapers from popular APIs (Unsplash, Pixabax, Pexels) with automatic switching in interval mode.
- **Custom settings**: Users can customize background blur, brightness, saturation, and color
- **Multiple background types support**: Including image, color, and gradient backgrounds, with image backgrounds supporting both remote images and local vault images
- **Compatible with existing themes**: This plugin can be used with existing Obsidian themes (but will override some styles, see [Notes](#notes))

View file

@ -10,6 +10,7 @@
- **时间段背景**: 根据时间段自动切换背景
- **手动切换**: 允许用户手动切换背景
- **🆕 随机壁纸支持**: 从流行的API获取随机壁纸Unsplash、Pixabay、Pexels支持间隔模式下的自动切换
- **自定义设置**: 用户可以自定义背景模糊度、亮度、饱和度和颜色
- **支持多种背景类型**: 包括图片、颜色背景和渐变背景,其中图片背景支持远程图片和仓库本地图片
- **可与已有主题兼容**: 该插件可以与现有的 Obsidian 主题一起使用(但会覆盖一些样式,详见[注意](#注意)

View file

@ -1,9 +1,9 @@
import builtins from "builtin-modules";
import esbuild from "esbuild";
import process from "process";
import builtins from "builtin-modules";
const banner =
`/*
`/*
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
if you want to view the source, please visit the github repository of this plugin
*/
@ -12,38 +12,39 @@ if you want to view the source, please visit the github repository of this plugi
const prod = (process.argv[2] === "production");
const context = await esbuild.context({
banner: {
js: banner,
},
entryPoints: ["main.ts"],
bundle: true,
external: [
"obsidian",
"electron",
"@codemirror/autocomplete",
"@codemirror/collab",
"@codemirror/commands",
"@codemirror/language",
"@codemirror/lint",
"@codemirror/search",
"@codemirror/state",
"@codemirror/view",
"@lezer/common",
"@lezer/highlight",
"@lezer/lr",
...builtins],
format: "cjs",
target: "es2018",
logLevel: "info",
sourcemap: prod ? false : "inline",
treeShaking: true,
outfile: "main.js",
minify: prod,
banner: {
js: banner,
},
entryPoints: ["src/main.ts"],
bundle: true,
external: [
"obsidian",
"electron",
"@codemirror/autocomplete",
"@codemirror/collab",
"@codemirror/commands",
"@codemirror/language",
"@codemirror/lint",
"@codemirror/search",
"@codemirror/state",
"@codemirror/view",
"@lezer/common",
"@lezer/highlight",
"@lezer/lr",
...builtins],
format: "cjs",
target: "es2018",
logLevel: "info",
sourcemap: prod ? false : "inline",
treeShaking: true,
outfile: "main.js",
minify: prod,
pure: prod ? ["console.debug"] : [],
});
if (prod) {
await context.rebuild();
process.exit(0);
await context.rebuild();
process.exit(0);
} else {
await context.watch();
await context.watch();
}

View file

@ -1,91 +0,0 @@
{
"settings_title": "Dynamic Theme Background Settings",
"enable_plugin_name": "Enable dynamic background",
"enable_plugin_desc": "Turn the dynamic background feature on or off.",
"blur_depth_name": "Background Blur",
"blur_depth_desc": "Set the background blurriness, from 0 to 30.",
"reset_blur_tooltip": "Reset to default blur",
"brightness_name": "Background Brightness",
"brightness_desc": "Set the background brightness, from 0 to 1.5.",
"reset_brightness_tooltip": "Reset to default brightness",
"saturate_name": "Background Saturation",
"saturate_desc": "Set the background saturation, from 0 to 2.",
"reset_saturate_tooltip": "Reset to default saturation",
"bg_color_name": "Background Color & Opacity",
"bg_color_desc": "Set the background color and adjust its opacity with the slider on the right.",
"reset_bg_color_tooltip": "Reset background color and opacity to default",
"bg_management_title": "Background Management",
"add_new_bg_name": "Add new background",
"add_image_bg_button": "Add Image",
"add_color_bg_button": "Add Color",
"add_gradient_bg_button": "Add Gradient",
"add_folder_bg_button": "Add Image Folder",
"restore_default_bg_button": "Restore Defaults",
"restore_default_bg_tooltip": "Add default gradient backgrounds to the list",
"restore_default_bg_success": "Successfully added {count} default backgrounds",
"restore_default_bg_no_new": "All default backgrounds already exist in the list",
"add_folder_modal_title": "Add Images from Folder",
"folder_path_label": "Folder Path (vault internal path):",
"folder_path_placeholder": "e.g., assets/images or photos",
"folder_scan_success": "Successfully added {count} images from folder",
"folder_scan_error": "Failed to scan folder: {error}",
"folder_not_found": "Folder not found or empty",
"folder_no_new_images": "All images in this folder already exist in the background list",
"mode_settings_title": "Mode Settings",
"switch_mode_name": "Switch Mode",
"switch_mode_desc": "Choose how the background switches.",
"mode_time_based": "Time-based",
"mode_interval": "Interval",
"mode_manual": "Manual",
"time_rules_title": "Time Rules",
"reset_time_rules_name": "Restore Default Time Rules",
"reset_time_rules_desc": "Reset the time rules to their default settings.",
"reset_button": "Restore Defaults",
"interval_name": "Switch Interval",
"interval_desc": "How often to switch the background (in minutes).",
"command_toggle_name": "Toggle dynamic background",
"command_toggle_enabled_notice": "Dynamic background enabled",
"command_toggle_disabled_notice": "Dynamic background disabled",
"command_next_bg_name": "Switch to next background",
"command_next_bg_notice": "Background switched to: {bgName}",
"command_test_bg_name": "Test current time period background",
"command_test_bg_success_notice": "Current period: {ruleName}, Background: {bgName}",
"command_test_bg_no_bg_notice": "No background set for period {ruleName}",
"command_test_bg_no_rule_notice": "No matching rule for the current time",
"edit_time_rule_title": "Edit Time Rule",
"default_morning_rule": "Morning",
"default_later_morning_rule": "Later Morning",
"default_noon_rule": "Noon",
"default_afternoon_rule": "Afternoon",
"default_dusk_rule": "Dusk",
"default_evening_rule": "Evening",
"default_night_rule": "Night",
"blue_purple_gradient_bg": "Blue Purple Gradient",
"pink_gradient_bg": "Pink Gradient",
"blue_cyan_gradient_bg": "Blue Cyan Gradient",
"green_cyan_gradient_bg": "Green Cyan Gradient",
"pink_yellow_gradient_bg": "Pink Yellow Gradient",
"cyan_pink_gradient_bg": "Cyan Pink Gradient",
"dark_blue_gray_gradient_bg": "Dark Blue Gray Gradient",
"add_modal_title_image": "Add Image Background",
"add_modal_title_color": "Add Color Background",
"add_modal_title_gradient": "Add Gradient Background",
"bg_name_label": "Background Name:",
"image_url_label": "Image URL (remote link or vault internal path):",
"color_value_label": "Color value (e.g. #ff0000):",
"gradient_css_label": "CSS gradient (e.g. linear-gradient(45deg, #ff0000, #0000ff)):",
"rule_name_label": "Rule Name:",
"start_time_label": "Start Time (HH:MM):",
"end_time_label": "End Time (HH:MM):",
"cancel_button": "Cancel",
"confirm_button": "Confirm",
"preview_button": "Preview",
"delete_button": "Delete",
"edit_button": "Edit",
"select_background_option": "Select Background",
"command_open_settings_tab_name": "Open Settings in New Tab",
"reload_plugin_tooltip": "Reload plugin",
"reload_plugin_notice": "Plugin reloaded",
"drag_hint_text": "💡 Tip: You can drag background items to reorder them",
"drag_handle_tooltip": "Drag to reorder"
}

View file

@ -1,91 +0,0 @@
{
"settings_title": "动态主题背景设置",
"enable_plugin_name": "启用动态背景",
"enable_plugin_desc": "开启或关闭动态背景功能。",
"blur_depth_name": "背景模糊",
"blur_depth_desc": "设置背景的模糊程度,范围 0-30。",
"reset_blur_tooltip": "重置为默认模糊度",
"brightness_name": "背景亮度",
"brightness_desc": "设置背景亮度,范围 0-1.5。",
"reset_brightness_tooltip": "重置为默认亮度",
"saturate_name": "背景饱和度",
"saturate_desc": "设置背景饱和度,范围 0-2。",
"reset_saturate_tooltip": "重置为默认饱和度",
"bg_color_name": "背景颜色和透明度",
"bg_color_desc": "设置背景颜色,透明度可通过右侧滑块调整。",
"reset_bg_color_tooltip": "重置背景颜色和透明度为默认值",
"bg_management_title": "背景管理",
"add_new_bg_name": "添加新背景",
"add_image_bg_button": "添加图片",
"add_color_bg_button": "添加颜色",
"add_gradient_bg_button": "添加渐变",
"add_folder_bg_button": "添加图片文件夹",
"restore_default_bg_button": "恢复默认",
"restore_default_bg_tooltip": "将默认渐变背景添加到列表中",
"restore_default_bg_success": "成功添加了 {count} 个默认背景",
"restore_default_bg_no_new": "所有默认背景已存在于列表中",
"add_folder_modal_title": "从文件夹添加图片",
"folder_path_label": "文件夹路径(库内路径):",
"folder_path_placeholder": "例如assets/images 或 photos",
"folder_scan_success": "成功从文件夹添加了 {count} 张图片",
"folder_scan_error": "扫描文件夹失败:{error}",
"folder_not_found": "文件夹未找到或为空",
"folder_no_new_images": "此文件夹中的所有图片已存在于背景列表中",
"mode_settings_title": "模式设置",
"switch_mode_name": "切换模式",
"switch_mode_desc": "选择背景的切换方式。",
"mode_time_based": "基于时间",
"mode_interval": "定时切换",
"mode_manual": "手动",
"time_rules_title": "时间规则",
"reset_time_rules_name": "恢复默认时间规则",
"reset_time_rules_desc": "将时间规则重置为默认设置。",
"reset_button": "恢复默认",
"interval_name": "切换间隔",
"interval_desc": "背景切换的间隔时间(分钟)。",
"command_toggle_name": "切换动态背景开关",
"command_toggle_enabled_notice": "动态背景已启用",
"command_toggle_disabled_notice": "动态背景已禁用",
"command_next_bg_name": "切换到下一个背景",
"command_next_bg_notice": "背景已切换为:{bgName}",
"command_test_bg_name": "测试当前时间段背景",
"command_test_bg_success_notice": "当前时间段:{ruleName},背景:{bgName}",
"command_test_bg_no_bg_notice": "时间段 {ruleName} 没有设置背景",
"command_test_bg_no_rule_notice": "当前时间没有匹配的规则",
"edit_time_rule_title": "编辑时间规则",
"default_morning_rule": "清晨",
"default_later_morning_rule": "上午",
"default_noon_rule": "中午",
"default_afternoon_rule": "下午",
"default_dusk_rule": "黄昏",
"default_evening_rule": "傍晚",
"default_night_rule": "夜晚",
"blue_purple_gradient_bg": "蓝紫色渐变",
"pink_gradient_bg": "粉红色渐变",
"blue_cyan_gradient_bg": "蓝青色渐变",
"green_cyan_gradient_bg": "绿青色渐变",
"pink_yellow_gradient_bg": "粉黄色渐变",
"cyan_pink_gradient_bg": "青粉色渐变",
"dark_blue_gray_gradient_bg": "深蓝灰色渐变",
"add_modal_title_image": "添加图片背景",
"add_modal_title_color": "添加颜色背景",
"add_modal_title_gradient": "添加渐变背景",
"bg_name_label": "背景名称:",
"image_url_label": "图片链接(远程链接或库内路径):",
"color_value_label": "颜色值(如 #ff0000",
"gradient_css_label": "CSS渐变如 linear-gradient(45deg, #ff0000, #0000ff)",
"rule_name_label": "规则名称:",
"start_time_label": "开始时间HH:MM",
"end_time_label": "结束时间HH:MM",
"cancel_button": "取消",
"confirm_button": "确定",
"preview_button": "预览",
"delete_button": "删除",
"edit_button": "编辑",
"select_background_option": "选择背景",
"command_open_settings_tab_name": "在新标签页中打开设置",
"reload_plugin_tooltip": "重新加载插件",
"reload_plugin_notice": "插件已重新加载",
"drag_hint_text": "💡 提示:您可以拖拽背景项目来重新排序",
"drag_handle_tooltip": "拖拽以重新排序"
}

1690
main.ts

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,51 @@
/**
*
*/
import { Command, Notice } from "obsidian";
import { t } from "../i18n";
import DynamicThemeBackgroundPlugin from "../plugin";
import { apiManager } from "../wallpaper-apis";
export function createFetchWallpaperCommand(plugin: DynamicThemeBackgroundPlugin): Command {
return {
id: "fetch-random-wallpaper",
name: t("command_fetch_random_wallpaper_name"),
callback: async () => {
if (!plugin.settings.enableRandomWallpaper) {
new Notice(t("command_fetch_random_wallpaper_notice_disabled"));
return;
}
// 显示加载提示
const loadingNotice = new Notice("🔄 Fetching wallpaper from API...", 0);
try {
// 使用API管理器获取随机壁纸这样可以受益于状态管理
const wallpaperImages = await apiManager.getRandomWallpapers();
// 关闭加载提示
loadingNotice.hide();
if (wallpaperImages && wallpaperImages.length > 0) {
// 创建一个临时的背景项用于随机壁纸
plugin.background = {
id: `random-wallpaper-${Date.now()}`,
name: `Random Wallpaper`,
type: "image",
value: wallpaperImages[0].url,
};
plugin.updateStyleCss();
new Notice("✅ Successfully applied random wallpaper!");
} else {
new Notice("❌ No wallpaper APIs available or all APIs failed");
}
} catch (error) {
// 关闭加载提示
loadingNotice.hide();
console.error("DTB: Error fetching random wallpaper:", error);
new Notice("❌ Error fetching wallpaper from API");
}
},
};
}

29
src/commands/index.ts Normal file
View file

@ -0,0 +1,29 @@
/**
* -
*/
import { Command } from "obsidian";
import DynamicThemeBackgroundPlugin from "../plugin";
// 导入所有命令创建函数
import { createFetchWallpaperCommand } from "./fetch-wallpaper-command";
import { createNextBackgroundCommand } from "./next-background-command";
import { createOpenSettingsCommand } from "./open-settings-command";
import { createTestBackgroundCommand } from "./test-background-command";
import { createToggleCommand } from "./toggle-command";
/**
*
*/
export function registerCommands(plugin: DynamicThemeBackgroundPlugin): void {
const commands: Command[] = [
createToggleCommand(plugin),
createNextBackgroundCommand(plugin),
createTestBackgroundCommand(plugin),
createFetchWallpaperCommand(plugin),
createOpenSettingsCommand(plugin),
];
commands.forEach((command) => {
plugin.addCommand(command);
});
}

View file

@ -0,0 +1,26 @@
/**
*
*/
import { Command, Notice } from "obsidian";
import { t } from "../i18n";
import DynamicThemeBackgroundPlugin from "../plugin";
export function createNextBackgroundCommand(plugin: DynamicThemeBackgroundPlugin): Command {
return {
id: "next-background",
name: t("command_next_bg_name"),
callback: () => {
if (plugin.settings.backgrounds.length > 0) {
plugin.settings.currentIndex = (plugin.settings.currentIndex + 1) % plugin.settings.backgrounds.length;
plugin.background = plugin.settings.backgrounds[plugin.settings.currentIndex];
plugin.updateStyleCss();
plugin.saveSettings();
new Notice(
t("command_next_bg_notice", {
bgName: plugin.background.name,
})
);
}
},
};
}

View file

@ -0,0 +1,16 @@
/**
*
*/
import { Command } from "obsidian";
import { t } from "../i18n";
import DynamicThemeBackgroundPlugin from "../plugin";
export function createOpenSettingsCommand(plugin: DynamicThemeBackgroundPlugin): Command {
return {
id: "open-dtb-settings-tab",
name: t("command_open_settings_tab_name"),
callback: () => {
plugin.activateView();
},
};
}

View file

@ -0,0 +1,46 @@
/**
*
*/
import { Command, Notice } from "obsidian";
import { t } from "../i18n";
import DynamicThemeBackgroundPlugin from "../plugin";
export function createTestBackgroundCommand(plugin: DynamicThemeBackgroundPlugin): Command {
return {
id: "test-current-background",
name: t("command_test_bg_name"),
callback: () => {
switch (plugin.settings.mode) {
case "time-based": {
const rule = plugin.getCurrentTimeRule();
if (rule) {
const bg = plugin.settings.backgrounds.find((b) => b.id === rule.backgroundId);
if (bg) {
plugin.background = bg;
plugin.updateStyleCss();
new Notice(
t("command_test_bg_success_notice", {
ruleName: rule.name,
bgName: bg.name,
})
);
} else {
new Notice(
t("command_test_bg_no_bg_notice", {
ruleName: rule.name,
})
);
}
} else {
new Notice(t("command_test_bg_no_rule_notice"));
}
break;
}
default: {
new Notice(t("command_test_bg_not_supported_notice"));
break;
}
}
},
};
}

View file

@ -0,0 +1,25 @@
/**
*
*/
import { Command, Notice } from "obsidian";
import { t } from "../i18n";
import DynamicThemeBackgroundPlugin from "../plugin";
export function createToggleCommand(plugin: DynamicThemeBackgroundPlugin): Command {
return {
id: "toggle-dtb",
name: t("command_toggle_name"),
callback: () => {
plugin.settings.enabled = !plugin.settings.enabled;
plugin.saveSettings();
if (plugin.settings.enabled) {
plugin.startBackgroundManager();
new Notice(t("command_toggle_enabled_notice"));
} else {
plugin.stopBackgroundManager();
new Notice(t("command_toggle_disabled_notice"));
}
},
};
}

144
src/default-settings.ts Normal file
View file

@ -0,0 +1,144 @@
/**
* -
* API配置
*/
import { t } from "./i18n";
import type { DTBSettings } from "./types";
import { apiRegistry, WallpaperApiType } from "./wallpaper-apis";
let DEFAULT_SETTINGS: DTBSettings;
function genDefaultSettings(): DTBSettings {
DEFAULT_SETTINGS = {
enabled: true,
blurDepth: 0, // default blur
brightness4Bg: 0.9, // default brightness
saturate4Bg: 1, // default saturation
bgColor: "#1f1e1e", // default background color (without alpha)
bgColorOpacity: 0.5, // default background color opacity (0-1)
mode: "time-based",
timeRules: [
{
id: "morning",
name: t("default_morning_rule"),
startTime: "06:00",
endTime: "09:00",
backgroundId: "blue-purple-gradient",
enabled: true,
},
{
id: "later-morning",
name: t("default_later_morning_rule"),
startTime: "09:00",
endTime: "11:00",
backgroundId: "pink-gradient",
enabled: true,
},
{
id: "noon",
name: t("default_noon_rule"),
startTime: "11:00",
endTime: "13:00",
backgroundId: "blue-cyan-gradient",
enabled: true,
},
{
id: "afternoon",
name: t("default_afternoon_rule"),
startTime: "13:00",
endTime: "17:00",
backgroundId: "green-cyan-gradient",
enabled: true,
},
{
id: "dusk",
name: t("default_dusk_rule"),
startTime: "17:00",
endTime: "18:00",
backgroundId: "pink-yellow-gradient",
enabled: true,
},
{
id: "evening",
name: t("default_evening_rule"),
startTime: "18:00",
endTime: "22:00",
backgroundId: "cyan-pink-gradient",
enabled: true,
},
{
id: "night",
name: t("default_night_rule"),
startTime: "22:00",
endTime: "06:00",
backgroundId: "dark-blue-gray-gradient",
enabled: true,
},
],
intervalMinutes: 60,
backgrounds: [
{
id: "blue-purple-gradient",
name: t("blue_purple_gradient_bg"),
type: "gradient",
value: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
},
{
id: "pink-gradient",
name: t("pink_gradient_bg"),
type: "gradient",
value: "linear-gradient(135deg, #f093fb 0%, #f5576c 100%)",
},
{
id: "blue-cyan-gradient",
name: t("blue_cyan_gradient_bg"),
type: "gradient",
value: "linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)",
},
{
id: "green-cyan-gradient",
name: t("green_cyan_gradient_bg"),
type: "gradient",
value: "linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)",
},
{
id: "pink-yellow-gradient",
name: t("pink_yellow_gradient_bg"),
type: "gradient",
value: "linear-gradient(135deg, #fa709a 0%, #fee140 100%)",
},
{
id: "cyan-pink-gradient",
name: t("cyan_pink_gradient_bg"),
type: "gradient",
value: "linear-gradient(135deg, #a8edea 0%, #fed6e3 100%)",
},
{
id: "dark-blue-gray-gradient",
name: t("dark_blue_gray_gradient_bg"),
type: "gradient",
value: "linear-gradient(135deg, #2c3e50 0%, #34495e 100%)",
},
],
currentIndex: 0,
// 随机壁纸设置
enableRandomWallpaper: false, // 默认不启用随机壁纸
wallpaperApis: [
{
id: "api-wallhaven-default",
name: "Wallhaven API (Default)",
type: WallpaperApiType.Wallhaven,
baseUrl: apiRegistry.getDefaultBaseUrl(WallpaperApiType.Wallhaven) || "",
enabled: false,
params: apiRegistry.getDefaultParams(WallpaperApiType.Wallhaven),
},
],
};
return DEFAULT_SETTINGS;
}
export function getDefaultSettings(): DTBSettings {
return DEFAULT_SETTINGS || genDefaultSettings();
}

135
src/i18n/en.ts Normal file
View file

@ -0,0 +1,135 @@
export default {
// 基础设置
settings_title: "Dynamic Theme Background Settings",
enable_plugin_name: "Enable dynamic background",
enable_plugin_desc: "Turn the dynamic background feature on or off.",
basic_settings_title: "Basic Settings",
reload_plugin_tooltip: "Refresh Background",
// 外观设置
appearance_settings_title: "Appearance Settings",
blur_depth_name: "Background Blur",
blur_depth_desc: "Set the background blurriness, from 0 to 30.",
reset_blur_tooltip: "Reset to default blur",
brightness_name: "Background Brightness",
brightness_desc: "Set the background brightness, from 0 to 1.5.",
reset_brightness_tooltip: "Reset to default brightness",
saturate_name: "Background Saturation",
saturate_desc: "Set the background saturation, from 0 to 2.",
reset_saturate_tooltip: "Reset to default saturation",
bg_mask_color_name: "Background Mask Color & Opacity",
bg_mask_color_desc: "Set the background mask color and adjust its opacity with the slider on the right.",
reset_bg_mask_color_tooltip: "Reset background mask color and opacity to default",
// 模式设置
mode_settings_title: "Mode Settings",
switch_mode_name: "Switch Mode",
switch_mode_desc: "Choose how the background switches.",
mode_time_based: "Time-Slot",
mode_interval: "Interval",
mode_manual: "Manual",
time_rules_title: "Time Slot Rules",
manage_time_rules_name: "Manage Time Slot Rules",
manage_time_rules_desc: "Add, edit or remove time slot rules for background switching.",
add_time_rule_button: "Add Rule",
add_time_rule_tooltip: "Click to add a new time slot rule",
clear_time_rules_button: "Clear Rules",
clear_time_rules_tooltip: "Clear all time slot rules",
reset_time_rules_button: "Restore Defaults",
reset_time_rules_tooltip: "Reset the time slot rules to their default settings.",
time_rule_hint: "💡 When time slot rules overlap, the first matching rule takes precedence.",
rule_name_label: "Rule Name:",
start_time_label: "Start Time (HH:MM):",
end_time_label: "End Time (HH:MM):",
interval_name: "Switch Interval",
interval_desc: "How often to switch the background (in minutes).",
// 背景管理
bg_management_title: "Background Management",
add_new_bg_name: "Add new background",
add_new_bg_desc: "Add a new background to the list.",
add_image_bg_button: "Add Image",
add_color_bg_button: "Add Color",
add_gradient_bg_button: "Add Gradient",
add_folder_bg_button: "Add Image Folder",
restore_default_bg_button: "Restore Defaults",
restore_default_bg_tooltip: "Add default gradient backgrounds to the list",
restore_default_bg_success: "Successfully added {count} default backgrounds",
restore_default_bg_no_new: "All default backgrounds already exist in the list",
add_folder_modal_title: "Add Images from Folder",
folder_path_label: "Folder Path (vault internal path):",
folder_path_placeholder: "e.g., assets/images or photos",
folder_scan_success: "Successfully added {count} images from folder",
folder_scan_error: "Failed to scan folder: {error}",
folder_not_found: "Folder not found or empty",
folder_no_new_images: "All images in this folder already exist in the background list",
edit_time_rule_title: "Edit Time Rule",
add_modal_title_image: "Add Image Background",
add_modal_title_color: "Add Color Background",
add_modal_title_gradient: "Add Gradient Background",
bg_name_label: "Background Name:",
image_url_label: "Image URL (remote link or vault internal path):",
color_value_label: "Color value (e.g. #ff0000):",
gradient_css_label: "CSS gradient (e.g. linear-gradient(45deg, #ff0000, #0000ff)):",
select_background_option: "Select Background",
drag_hint_text: "💡 Tip: You can drag background items to reorder them",
drag_handle_tooltip: "Drag to reorder",
random_wallpaper_settings_title: "Random Wallpaper Settings",
enable_random_wallpaper_name: "Enable Random Wallpaper",
enable_random_wallpaper_desc:
"When enabled, random wallpapers will be fetched from wallpaper APIs and the background list, otherwise it will use the background list in order.",
// 壁纸API管理
wallpaper_api_management_title: "Wallpaper API Management",
add_api_name: "Add API",
add_api_desc: "Add a new wallpaper API configuration",
add_api_button: "Add API",
wallpaper_api_hint:
"💡 You can add multiple API instances of the same type with different parameters to get different types of wallpapers.",
add_api_bg_tooltip: "Click to add a new wallpaper from API",
wallpaper_api_url_name: "Wallpaper API URL",
wallpaper_api_url_desc:
"Supports Unsplash, Pixabay, Pexels APIs. Default uses Unsplash API (need to replace YOUR_ACCESS_KEY)",
fallback_on_error_name: "Fallback on API Error",
fallback_on_error_desc: "When API request fails, fallback to use local background list",
test_api_connection_name: "Test API Connection",
test_api_connection_desc: "Test whether current API settings can fetch wallpapers normally",
// 命令
command_open_settings_tab_name: "Open Settings in New Tab",
command_toggle_name: "Toggle dynamic background",
command_toggle_enabled_notice: "Dynamic background enabled",
command_toggle_disabled_notice: "Dynamic background disabled",
command_next_bg_name: "Switch to next background",
command_next_bg_notice: "Background switched to: {bgName}",
command_test_bg_name: "Test current time period background",
command_test_bg_success_notice: "Current period: {ruleName}, Background: {bgName}",
command_test_bg_no_bg_notice: "No background set for period {ruleName}",
command_test_bg_no_rule_notice: "No matching rule for the current time",
command_test_bg_not_supported_notice: "Current mode does not support background testing",
command_fetch_random_wallpaper_name: "Fetch Random Wallpaper",
command_fetch_random_wallpaper_notice_disabled: "Please enable random wallpaper feature in settings first",
command_fetch_random_wallpaper_notice_success: "Applied random wallpaper",
command_fetch_random_wallpaper_notice_failed:
"Failed to fetch random wallpaper, please check API settings and network connection",
// 通用按钮文本
button_cancel: "Cancel",
button_confirm: "Confirm",
button_preview: "Preview",
button_delete: "Delete",
button_edit: "Edit",
button_browse: "Browse",
button_test: "Test",
button_update: "Update",
button_add: "Add",
// 默认配置相关
default_morning_rule: "Morning",
default_later_morning_rule: "Later Morning",
default_noon_rule: "Noon",
default_afternoon_rule: "Afternoon",
default_dusk_rule: "Dusk",
default_evening_rule: "Evening",
default_night_rule: "Night",
blue_purple_gradient_bg: "Blue Purple Gradient",
pink_gradient_bg: "Pink Gradient",
blue_cyan_gradient_bg: "Blue Cyan Gradient",
green_cyan_gradient_bg: "Green Cyan Gradient",
pink_yellow_gradient_bg: "Pink Yellow Gradient",
cyan_pink_gradient_bg: "Cyan Pink Gradient",
dark_blue_gray_gradient_bg: "Dark Blue Gray Gradient",
};

24
src/i18n/index.ts Normal file
View file

@ -0,0 +1,24 @@
/**
* -
* Obsidian语言设置自动加载对应翻译
*/
import { moment } from "obsidian";
import en from "./en";
import zhCN from "./zh-cn";
const localeMap: Record<string, typeof en> = {
en,
"zh-cn": zhCN,
};
const locale = localeMap[moment.locale()];
export function t(str: keyof typeof en, vars?: Record<string, string>): string {
let text = (locale && locale[str]) || en[str];
if (vars) {
for (const varKey in vars) {
text = text.replace(`{${varKey}}`, vars[varKey]);
}
}
return text;
}

131
src/i18n/zh-cn.ts Normal file
View file

@ -0,0 +1,131 @@
export default {
// 基础设置
settings_title: "动态主题背景设置",
enable_plugin_name: "启用动态背景",
enable_plugin_desc: "开启或关闭动态背景功能",
basic_settings_title: "基础设置",
reload_plugin_tooltip: "刷新背景",
// 外观设置
appearance_settings_title: "外观设置",
blur_depth_name: "背景模糊",
blur_depth_desc: " 设置背景的模糊程度,范围 0-30",
reset_blur_tooltip: "重置为默认模糊度",
brightness_name: "背景亮度",
brightness_desc: " 设置背景亮度,范围 0-15",
reset_brightness_tooltip: "重置为默认亮度",
saturate_name: "背景饱和度",
saturate_desc: " 设置背景饱和度,范围 0-2",
reset_saturate_tooltip: "重置为默认饱和度",
bg_mask_color_name: "背景遮罩颜色和透明度",
bg_mask_color_desc: "设置背景遮罩的颜色和透明度,透明度可通过右侧滑块调整",
reset_bg_mask_color_tooltip: "重置背景遮罩颜色和透明度为默认值",
// 模式设置
mode_settings_title: "模式设置",
switch_mode_name: "切换模式",
switch_mode_desc: "选择背景的切换方式",
mode_time_based: "基于时段",
mode_interval: "定时切换",
mode_manual: "手动",
time_rules_title: "时段规则",
manage_time_rules_name: "管理时段规则",
manage_time_rules_desc: "添加、编辑或删除时段规则以切换背景",
add_time_rule_button: "添加规则",
add_time_rule_tooltip: "点击添加新的时段规则",
clear_time_rules_button: "清除规则",
clear_time_rules_tooltip: "清除所有时段规则",
reset_time_rules_button: "恢复默认",
reset_time_rules_tooltip: "将时段规则重置为默认设置",
time_rule_hint: "💡 当时段规则存在重叠,优先使用第一个匹配的规则 ",
rule_name_label: " 规则名称:",
start_time_label: " 开始时间HH:MM",
end_time_label: " 结束时间HH:MM",
interval_name: "切换间隔",
interval_desc: " 背景切换的间隔时间(分钟)",
// 背景管理
bg_management_title: "背景管理",
add_new_bg_name: "添加新背景",
add_new_bg_desc: "将新背景添加到列表中",
add_image_bg_button: "添加图片",
add_color_bg_button: "添加颜色",
add_gradient_bg_button: "添加渐变",
add_folder_bg_button: "添加图片文件夹",
restore_default_bg_button: "恢复默认",
restore_default_bg_tooltip: "将默认渐变背景添加到列表中",
restore_default_bg_success: "成功添加了 {count} 个默认背景",
restore_default_bg_no_new: "所有默认背景已存在于列表中",
add_folder_modal_title: "从文件夹添加图片",
folder_path_label: " 文件夹路径(库内路径):",
folder_path_placeholder: " 例如assets/images 或 photos",
folder_scan_success: "成功从文件夹添加了 {count} 张图片",
folder_scan_error: " 扫描文件夹失败:{error}",
folder_not_found: "文件夹未找到或为空",
folder_no_new_images: "此文件夹中的所有图片已存在于背景列表中",
edit_time_rule_title: "编辑时段规则",
add_modal_title_image: "添加图片背景",
add_modal_title_color: "添加颜色背景",
add_modal_title_gradient: "添加渐变背景",
bg_name_label: " 背景名称:",
image_url_label: " 图片链接(远程链接或库内路径):",
color_value_label: " 颜色值(如 #ff0000",
gradient_css_label: "CSS 渐变(如 linear-gradient(45deg, #ff0000, #0000ff)",
select_background_option: "选择背景",
drag_hint_text: "💡 提示:您可以拖拽背景项目来重新排序 ",
drag_handle_tooltip: "拖拽以重新排序",
random_wallpaper_settings_title: "随机壁纸设置",
enable_random_wallpaper_name: "启用随机壁纸",
enable_random_wallpaper_desc: "启用后,将从壁纸网站 API 和背景列表中获取随机壁纸,否则将从背景列表按顺序获取",
// 壁纸 API 管理
wallpaper_api_management_title: "壁纸 API 管理",
add_api_name: " 添加 API",
add_api_desc: "添加新的壁纸 API 配置",
add_api_button: " 添加 API",
wallpaper_api_hint: "💡 您可以创建同一种类型的 API 但参数不同的多个 API 实例,以获取不同类型的壁纸 ",
add_api_bg_tooltip: "点击从 API 添加新的壁纸",
wallpaper_api_url_name: "壁纸 API 地址",
wallpaper_api_url_desc: " 支持 Unsplash、Pixabay、Pexels 等 API 默认使用 Unsplash API需要替换 YOUR_ACCESS_KEY",
fallback_on_error_name: "API 失败时回退 ",
fallback_on_error_desc: "当 API 请求失败时,是否回退到使用本地背景列表",
test_api_connection_name: "测试 API 连接",
test_api_connection_desc: "测试当前 API 设置是否能正常获取壁纸",
// 命令
command_open_settings_tab_name: "在新标签页中打开设置",
command_toggle_name: "切换动态背景开关",
command_toggle_enabled_notice: "动态背景已启用",
command_toggle_disabled_notice: "动态背景已禁用",
command_next_bg_name: "切换到下一个背景",
command_next_bg_notice: " 背景已切换为:{bgName}",
command_test_bg_name: "测试当前时间段背景",
command_test_bg_success_notice: " 当前时间段:{ruleName},背景:{bgName}",
command_test_bg_no_bg_notice: "时间段 {ruleName} 没有设置背景",
command_test_bg_no_rule_notice: "当前时间没有匹配的规则",
command_test_bg_not_supported_notice: "非时段模式不支持该测试命令",
command_fetch_random_wallpaper_name: "获取随机壁纸",
command_fetch_random_wallpaper_notice_disabled: "请先在设置中启用随机壁纸功能",
command_fetch_random_wallpaper_notice_success: "已应用随机壁纸",
command_fetch_random_wallpaper_notice_failed: "获取随机壁纸失败,请检查 API 设置和网络连接",
// 通用按钮文本
button_cancel: "取消",
button_confirm: "确定",
button_preview: "预览",
button_delete: "删除",
button_edit: "编辑",
button_browse: "浏览",
button_test: "测试",
button_update: "更新",
button_add: "添加",
// 默认配置相关
default_morning_rule: "清晨",
default_later_morning_rule: "上午",
default_noon_rule: "中午",
default_afternoon_rule: "下午",
default_dusk_rule: "黄昏",
default_evening_rule: "傍晚",
default_night_rule: "夜晚",
blue_purple_gradient_bg: "蓝紫色渐变",
pink_gradient_bg: "粉红色渐变",
blue_cyan_gradient_bg: "蓝青色渐变",
green_cyan_gradient_bg: "绿青色渐变",
pink_yellow_gradient_bg: "粉黄色渐变",
cyan_pink_gradient_bg: "青粉色渐变",
dark_blue_gray_gradient_bg: "深蓝灰色渐变",
};

1
src/main.ts Normal file
View file

@ -0,0 +1 @@
export { default } from "./plugin";

View file

@ -0,0 +1,112 @@
/*
*/
import { App, Modal } from "obsidian";
import { t } from "../i18n";
import { ImagePathSuggestModal } from "./image-path-suggest-modal";
export class BackgroundModal extends Modal {
type: "image" | "color" | "gradient";
onSubmit: (name: string, value: string) => void;
nameInput: HTMLInputElement;
valueInput: HTMLInputElement;
constructor(app: App, type: "image" | "color" | "gradient", onSubmit: (name: string, value: string) => void) {
super(app);
this.type = type;
this.onSubmit = onSubmit;
}
onOpen() {
const { contentEl } = this;
let titleKey: "add_modal_title_image" | "add_modal_title_color" | "add_modal_title_gradient";
switch (this.type) {
case "image":
titleKey = "add_modal_title_image";
break;
case "color":
titleKey = "add_modal_title_color";
break;
case "gradient":
titleKey = "add_modal_title_gradient";
break;
}
contentEl.createEl("h2", { text: t(titleKey) });
// Name input
contentEl.createEl("label", { text: t("bg_name_label") });
this.nameInput = contentEl.createEl("input", { type: "text", cls: "dtb-input" });
// Value input
let valueLabel = "";
let placeholder = "";
switch (this.type) {
case "image":
valueLabel = t("image_url_label");
placeholder = "https://example.com/image.jpg OR path/to/image.jpg";
break;
case "color":
valueLabel = t("color_value_label");
placeholder = "#ffffff";
break;
case "gradient":
valueLabel = t("gradient_css_label");
placeholder = "linear-gradient(45deg, #ff0000, #0000ff)";
break;
}
contentEl.createEl("label", { text: valueLabel });
// 为图片类型创建带有浏览按钮的输入区域
if (this.type === "image") {
const inputContainer = contentEl.createDiv("dtb-flex-container-spaced");
this.valueInput = inputContainer.createEl("input", {
type: "text",
placeholder,
cls: "dtb-flex-1",
});
const browseButton = inputContainer.createEl("button", {
type: "button",
text: t("button_browse"),
});
browseButton.onclick = () => {
const modal = new ImagePathSuggestModal(this.app, (imagePath: string) => {
this.valueInput.value = imagePath;
});
modal.open();
};
} else {
this.valueInput = contentEl.createEl("input", {
type: "text",
placeholder,
cls: "dtb-input",
});
}
// Buttons
const buttonContainer = contentEl.createDiv("dtb-flex-container-end");
const cancelButton = buttonContainer.createEl("button", {
text: t("button_cancel"),
cls: "dtb-action-button",
});
cancelButton.onclick = () => this.close();
const submitButton = buttonContainer.createEl("button", {
text: t("button_confirm"),
cls: ["dtb-action-button", "mod-cta"],
});
submitButton.onclick = () => {
this.onSubmit(this.nameInput.value, this.valueInput.value || placeholder);
this.close();
};
}
onClose() {
const { contentEl } = this;
contentEl.empty();
}
}

View file

@ -0,0 +1,32 @@
import { App, SuggestModal } from "obsidian";
import { t } from "../i18n";
export class ImageFolderSuggestModal extends SuggestModal<string> {
onSubmit: (folderPath: string) => void;
constructor(app: App, onSubmit: (folderPath: string) => void) {
super(app);
this.onSubmit = onSubmit;
this.setPlaceholder(t("folder_path_placeholder"));
}
getSuggestions(query: string): string[] {
if (query.trim() === "") {
return [];
}
const folders = this.app.vault.getAllFolders();
return folders
.map((folder) => folder.path)
.filter((path) => path.toLowerCase().includes(query.toLowerCase()))
.sort()
.slice(0, 10);
}
renderSuggestion(folderPath: string, el: HTMLElement) {
el.createEl("div", { text: folderPath });
}
onChooseSuggestion(folderPath: string, evt: MouseEvent | KeyboardEvent) {
this.onSubmit(folderPath);
}
}

View file

@ -0,0 +1,40 @@
import { App, SuggestModal } from "obsidian";
export class ImagePathSuggestModal extends SuggestModal<string> {
onSubmit: (imagePath: string) => void;
constructor(app: App, onSubmit: (imagePath: string) => void) {
super(app);
this.onSubmit = onSubmit;
this.setPlaceholder("https://example.com/image.jpg OR path/to/image.jpg");
}
getSuggestions(query: string): string[] {
if (query.trim() === "") {
return [];
}
if (query.startsWith("http://") || query.startsWith("https://") || query.startsWith("www.")) {
return [];
}
const files = this.app.vault.getFiles();
const imageExtensions = [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".svg"];
return files
.filter((file) => imageExtensions.some((ext) => file.path.toLowerCase().endsWith(ext)))
.map((file) => file.path)
.filter((path) => path.toLowerCase().includes(query.toLowerCase()))
.sort()
.slice(0, 10);
}
renderSuggestion(imagePath: string, el: HTMLElement) {
const container = el.createDiv({ cls: "dtb-suggestion-item" });
const icon = container.createSpan();
icon.textContent = "🖼️";
const text = container.createSpan();
text.textContent = imagePath;
}
onChooseSuggestion(imagePath: string, evt: MouseEvent | KeyboardEvent) {
this.onSubmit(imagePath);
}
}

10
src/modals/index.ts Normal file
View file

@ -0,0 +1,10 @@
/**
* -
* API编辑器等UI组件
*/
export { BackgroundModal } from "./background-modal";
export { ImageFolderSuggestModal } from "./image-folder-suggest-modal";
export { ImagePathSuggestModal } from "./image-path-suggest-modal";
export { TimeRuleModal } from "./time-rule-modal";
export { WallpaperApiEditorModal } from "./wallpaper-api-modal";

View file

@ -0,0 +1,78 @@
import { App, Modal } from "obsidian";
import { t } from "../i18n";
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;
constructor(
app: App,
rule: TimeRule,
onSubmit: (rule: { name: string; startTime: string; endTime: string }) => void
) {
super(app);
this.rule = rule;
this.onSubmit = onSubmit;
}
onOpen() {
const { contentEl } = this;
contentEl.createEl("h2", { text: t("edit_time_rule_title") });
// Name input
contentEl.createEl("label", { text: t("rule_name_label") });
this.nameInput = contentEl.createEl("input", {
type: "text",
value: this.rule.name,
cls: "dtb-input",
});
// Start time input
contentEl.createEl("label", { text: t("start_time_label") });
this.startTimeInput = contentEl.createEl("input", {
type: "time",
value: this.rule.startTime,
cls: "dtb-input",
});
// End time input
contentEl.createEl("label", { text: t("end_time_label") });
this.endTimeInput = contentEl.createEl("input", {
type: "time",
value: this.rule.endTime,
cls: "dtb-input",
});
// Buttons
const buttonContainer = contentEl.createDiv("dtb-flex-container-end");
const cancelButton = buttonContainer.createEl("button", {
text: t("button_cancel"),
cls: "dtb-action-button",
});
cancelButton.onclick = () => this.close();
const submitButton = buttonContainer.createEl("button", {
text: t("button_confirm"),
cls: ["dtb-action-button", "mod-cta"],
});
submitButton.onclick = () => {
this.onSubmit({
name: this.nameInput.value,
startTime: this.startTimeInput.value,
endTime: this.endTimeInput.value,
});
this.close();
};
}
onClose() {
const { contentEl } = this;
contentEl.empty();
}
}

View file

@ -0,0 +1,641 @@
import { App, Modal, Notice } from "obsidian";
import {
apiManager,
apiRegistry,
WallpaperApiConfig,
WallpaperApiParamDescriptor,
WallpaperApiType,
} from "../wallpaper-apis";
export class WallpaperApiEditorModal extends Modal {
apiConfig: WallpaperApiConfig;
onSubmit: (apiConfig: WallpaperApiConfig) => void;
// 基础配置输入元素
nameInput: HTMLInputElement;
descInput: HTMLInputElement; // 可选描述输入
typeSelect: HTMLSelectElement;
urlInput: HTMLInputElement;
// 动态参数输入元素映射
paramInputs: Map<string, HTMLElement> = new Map();
// 参数配置容器的引用
paramsSectionContainer: HTMLElement;
// 额外参数输入
extraParamsTextarea: HTMLTextAreaElement;
// Headers配置
headersContainer: HTMLDivElement;
headerInputs: Array<{ key: HTMLInputElement; value: HTMLInputElement }> = [];
// 自定义设置输入元素
customSettingsInputs: Map<string, HTMLInputElement> = new Map();
constructor(app: App, apiConfig: WallpaperApiConfig, onSubmit: (apiConfig: WallpaperApiConfig) => void) {
super(app);
this.apiConfig = apiConfig;
this.onSubmit = onSubmit;
}
onOpen() {
const { contentEl } = this;
contentEl.empty();
// 为模态框添加自定义CSS类以控制宽度
contentEl.addClass("dtb-api-modal-container");
contentEl.createEl("h2", {
text: this.apiConfig.id ? "Edit Wallpaper API" : "Add Wallpaper API",
});
// 第一部分:基础配置
this.createBasicConfigSection(contentEl);
// 第二部分API参数配置
this.createParamsSection(contentEl);
// 第三部分:自定义设置
this.createCustomSettingsSection(contentEl);
// 按钮
this.createButtonSection(contentEl);
}
// 创建基础配置部分
private createBasicConfigSection(container: HTMLElement) {
const section = container.createDiv("dtb-section-container");
section.createEl("h3", { text: "Basic Configuration" });
// API名称
section.createEl("label", { text: "API Name" });
this.nameInput = section.createEl("input", {
type: "text",
value: this.apiConfig.name || "",
placeholder: "e.g., Unsplash Nature",
cls: ["dtb-input"],
});
// 可选描述
section.createEl("label", { text: "Description (optional)" });
this.descInput = section.createEl("input", {
type: "text",
value: this.apiConfig.description || "",
placeholder: this.getDefaultDescriptionForType(this.apiConfig.type),
cls: ["dtb-input"],
});
// API类型选择
section.createEl("label", { text: "API Type" });
this.typeSelect = section.createEl("select", { cls: "dtb-dropdown" });
const apiTypes = Object.values(WallpaperApiType);
apiTypes.forEach((type) => {
const option = this.typeSelect.createEl("option", {
value: type,
text: type,
});
if (this.apiConfig.type === type) {
option.selected = true;
}
});
// 监听类型变化,重新生成参数配置
this.typeSelect.addEventListener("change", () => {
this.updateUrlPlaceholder();
this.updateDescriptionPlaceholder();
this.refreshParamsSection();
});
// API URL
section.createEl("label", { text: "API URL" });
// 获取默认 URL 作为 placeholder
const defaultUrl = this.getDefaultUrlForType(this.apiConfig.type);
this.urlInput = section.createEl("input", {
type: "text",
value: this.apiConfig.baseUrl || "",
placeholder: defaultUrl || "https://api.example.com/...",
cls: "dtb-input",
});
// Headers配置
this.createHeadersSection(section);
}
// 创建Headers配置部分
private createHeadersSection(container: HTMLElement) {
container.createEl("label", { text: "Headers (optional)" });
this.headersContainer = container.createDiv("dtb-list-container");
// 渲染已有的headers
if (this.apiConfig.headers) {
Object.entries(this.apiConfig.headers).forEach(([key, value]) => {
this.addHeaderInput(key, value);
});
}
// 添加header按钮
const addHeaderBtn = container.createEl("button", {
text: "Add Header",
type: "button",
cls: "dtb-action-button",
});
addHeaderBtn.onclick = () => this.addHeaderInput();
}
// 添加header输入行
private addHeaderInput(key = "", value = "") {
const headerRow = this.headersContainer.createDiv("dtb-list-row");
const keyInput = headerRow.createEl("input", {
type: "text",
value: key,
placeholder: "Header Key",
cls: "dtb-input",
});
const valueInput = headerRow.createEl("input", {
type: "text",
value: value,
placeholder: "Header Value",
cls: "dtb-input",
});
const removeBtn = headerRow.createEl("button", {
text: "×",
type: "button",
cls: "dtb-remove-button",
});
removeBtn.onclick = () => {
headerRow.remove();
const index = this.headerInputs.findIndex((h) => h.key === keyInput);
if (index > -1) {
this.headerInputs.splice(index, 1);
}
};
this.headerInputs.push({ key: keyInput, value: valueInput });
}
// 创建参数配置部分
private createParamsSection(container: HTMLElement) {
this.paramsSectionContainer = container.createDiv("dtb-section-container");
this.refreshParamsSection();
}
// 刷新参数配置部分
private refreshParamsSection() {
// 清除现有的参数输入
this.paramInputs.clear();
// 使用保存的容器引用而不是DOM查询
if (!this.paramsSectionContainer) return;
// 清空容器并重新创建所有内容
this.paramsSectionContainer.empty();
// 重新创建标题和文档链接
this.createParamsSectionHeader();
// 创建参数输入
const selectedType = this.typeSelect?.value || this.apiConfig.type;
const paramDescriptors = this.getParamDescriptorsForType(selectedType);
if (paramDescriptors.length > 0) {
this.createDynamicParamInputs(this.paramsSectionContainer, paramDescriptors);
}
// 额外参数JSON输入
this.createExtraParamsInput(this.paramsSectionContainer);
}
// 创建参数部分的标题和文档链接
private createParamsSectionHeader() {
const headerContainer = this.paramsSectionContainer.createDiv("dtb-section-header");
headerContainer.createEl("h3", { text: "API Parameters" });
// 创建链接容器,放在右侧
const linksContainer = headerContainer.createDiv("dtb-doc-links");
const selectedType = (this.typeSelect?.value as WallpaperApiType) || this.apiConfig.type;
// 添加 API 文档链接
const docUrl = apiRegistry.getApiDocUrl(selectedType);
if (docUrl) {
const docLink = linksContainer.createEl("a", {
text: "📖 API Documentation",
href: docUrl,
cls: "dtb-doc-link",
});
docLink.setAttribute("target", "_blank");
docLink.setAttribute("rel", "noopener noreferrer");
}
// 添加 token URL 链接
const tokenUrl = apiRegistry.getTokenUrl(selectedType);
if (tokenUrl) {
const tokenLink = linksContainer.createEl("a", {
text: "🔑 Token URL",
href: tokenUrl,
cls: "dtb-doc-link",
});
tokenLink.setAttribute("target", "_blank");
tokenLink.setAttribute("rel", "noopener noreferrer");
}
}
// 获取指定类型的默认 URL
private getDefaultUrlForType(type: WallpaperApiType): string {
return apiRegistry.getDefaultBaseUrl(type) || "https://api.example.com/...";
}
// 更新 URL placeholder
private updateUrlPlaceholder() {
if (this.urlInput) {
const selectedType = this.typeSelect.value as WallpaperApiType;
this.urlInput.placeholder = this.getDefaultUrlForType(selectedType);
}
}
// 获取默认描述
private getDefaultDescriptionForType(type: WallpaperApiType): string {
return apiRegistry.getDefaultDescription(type) || "No description provided.";
}
// 更新 描述 placeholder
private updateDescriptionPlaceholder() {
if (this.descInput) {
const selectedType = this.typeSelect.value as WallpaperApiType;
this.descInput.placeholder = this.getDefaultDescriptionForType(selectedType);
}
}
// 获取指定类型的参数描述
private getParamDescriptorsForType(type: string): WallpaperApiParamDescriptor[] {
const apiType = type as WallpaperApiType;
const descriptors = apiRegistry.getParamDescriptors(apiType);
return descriptors;
}
// 创建动态参数输入
private createDynamicParamInputs(container: HTMLElement, descriptors: WallpaperApiParamDescriptor[]) {
descriptors.forEach((descriptor) => {
const paramContainer = container.createDiv("dtb-field-container");
const labelText = descriptor.required ? `${descriptor.label} *` : descriptor.label;
const label = paramContainer.createEl("label", { text: labelText });
if (descriptor.description) {
label.createEl("small", {
text: ` (${descriptor.description})`,
cls: "dtb-field-description",
});
}
// 使用转换函数处理当前值
let currentValue = this.apiConfig.params[descriptor.key] || descriptor.defaultValue;
if (descriptor.fromApiValue && currentValue !== undefined) {
currentValue = descriptor.fromApiValue(currentValue);
}
let input: HTMLElement;
switch (descriptor.type) {
case "select":
input = this.createSelectInput(paramContainer, descriptor, currentValue);
break;
case "multiselect":
input = this.createMultiSelectInput(paramContainer, descriptor, currentValue);
break;
case "boolean":
input = this.createBooleanInput(paramContainer, descriptor, currentValue);
break;
case "number":
input = this.createNumberInput(paramContainer, descriptor, currentValue);
break;
case "password":
input = this.createStringInput(paramContainer, descriptor, currentValue);
(input as HTMLInputElement).type = "password";
break;
default: // string
input = this.createStringInput(paramContainer, descriptor, currentValue);
break;
}
this.paramInputs.set(descriptor.key, input);
});
}
// 创建字符串输入
private createStringInput(
container: HTMLElement,
descriptor: WallpaperApiParamDescriptor,
currentValue: string | number | boolean | string[] | undefined
): HTMLInputElement {
return container.createEl("input", {
type: "text",
value: currentValue?.toString() || descriptor.defaultValue?.toString() || "",
placeholder: descriptor.placeholder || "",
cls: "dtb-input",
});
}
// 创建数字输入
private createNumberInput(
container: HTMLElement,
descriptor: WallpaperApiParamDescriptor,
currentValue: string | number | boolean | string[] | undefined
): HTMLInputElement {
return container.createEl("input", {
type: "number",
value: currentValue?.toString() || descriptor.defaultValue?.toString() || "",
placeholder: descriptor.placeholder || "",
cls: "dtb-input",
});
}
// 创建布尔输入
private createBooleanInput(
container: HTMLElement,
descriptor: WallpaperApiParamDescriptor,
currentValue: string | number | boolean | string[] | undefined
): HTMLInputElement {
const checkbox = container.createEl("input", {
type: "checkbox",
cls: "dtb-checkbox",
});
const boolValue = currentValue !== undefined ? Boolean(currentValue) : Boolean(descriptor.defaultValue);
checkbox.checked = boolValue;
return checkbox;
}
// 创建选择输入
private createSelectInput(
container: HTMLElement,
descriptor: WallpaperApiParamDescriptor,
currentValue: string | number | boolean | string[] | undefined
): HTMLSelectElement {
const select = container.createEl("select", { cls: "dtb-dropdown" });
descriptor.options?.forEach((option) => {
const optionEl = select.createEl("option", {
value: option.value.toString(),
text: option.label,
});
if (
currentValue?.toString() === option.value.toString() ||
(!currentValue && descriptor.defaultValue?.toString() === option.value.toString())
) {
optionEl.selected = true;
}
});
return select;
}
// 创建多选输入
private createMultiSelectInput(
container: HTMLElement,
descriptor: WallpaperApiParamDescriptor,
currentValue: string | number | boolean | string[] | undefined
): HTMLElement {
const multiContainer = container.createDiv("dtb-multiselect-container");
const selectedValues = Array.isArray(currentValue)
? currentValue
: typeof currentValue === "string"
? currentValue.split(",")
: [];
descriptor.options?.forEach((option) => {
const checkboxContainer = multiContainer.createDiv("dtb-checkbox-container");
const checkbox = checkboxContainer.createEl("input", {
type: "checkbox",
value: option.value.toString(),
});
checkbox.checked = selectedValues.includes(option.value.toString());
checkboxContainer.createEl("label", { text: option.label });
});
return multiContainer;
}
// 创建额外参数JSON输入
private createExtraParamsInput(container: HTMLElement) {
container.createEl("label", { text: "Extra Parameters (JSON)" });
container.createEl("small", {
text: "Additional parameters not covered above, in JSON format",
cls: "dtb-field-description",
});
// 构建额外参数JSON
const knownKeys = Array.from(this.paramInputs.keys());
const extraParams: Record<string, string | number | boolean | string[]> = {};
Object.entries(this.apiConfig.params).forEach(([key, value]) => {
if (!knownKeys.includes(key) && value !== undefined) {
extraParams[key] = value;
}
});
this.extraParamsTextarea = container.createEl("textarea", {
value: Object.keys(extraParams).length > 0 ? JSON.stringify(extraParams, null, 2) : "",
placeholder: '{\n "customParam": "value",\n "anotherParam": 123\n}',
cls: "dtb-modal-textarea",
});
}
// 创建自定义设置部分
private createCustomSettingsSection(container: HTMLElement) {
const section = container.createDiv("dtb-section-container");
section.createEl("h3", { text: "Custom Settings (Not Implemented)" });
const customSettings = [
{ key: "imageUrlJsonPath", label: "Image URL JSON Path", placeholder: "images[].url" },
// TODO: 添加更多自定义设置项
];
customSettings.forEach((setting) => {
section.createEl("label", { text: setting.label });
const input = section.createEl("input", {
type: "text",
value: this.apiConfig.customSettings?.[setting.key] || "",
placeholder: setting.placeholder,
cls: "dtb-input",
});
this.customSettingsInputs.set(setting.key, input);
});
}
// 创建按钮部分
private createButtonSection(container: HTMLElement) {
const buttonContainer = container.createDiv("dtb-flex-container-end");
const testButton = buttonContainer.createEl("button", {
text: "Test API",
cls: "dtb-action-button",
});
testButton.onclick = async () => {
await this.testApiConfig();
};
const cancelButton = buttonContainer.createEl("button", {
text: "Cancel",
cls: "dtb-action-button",
});
cancelButton.onclick = () => this.close();
const submitButton = buttonContainer.createEl("button", {
text: "Save",
cls: ["dtb-action-button", "mod-cta"],
});
submitButton.onclick = () => {
this.saveApiConfig();
};
}
// 构建API配置对象
buildApiConfig(): WallpaperApiConfig {
// 收集动态参数
const params: Record<string, string | number | boolean | string[]> = {};
// 获取当前参数描述符以便使用转换函数
const selectedType = this.typeSelect.value;
const paramDescriptors = this.getParamDescriptorsForType(selectedType);
const descriptorMap = new Map(paramDescriptors.map((desc) => [desc.key, desc]));
// 从动态输入中收集参数
this.paramInputs.forEach((input, key) => {
let value: string | number | boolean | string[] | undefined;
if (input.tagName === "INPUT") {
const inputEl = input as HTMLInputElement;
if (inputEl.type === "checkbox") {
value = inputEl.checked;
} else if (inputEl.type === "number") {
value = inputEl.value ? parseInt(inputEl.value) : undefined;
} else {
value = inputEl.value || undefined;
}
} else if (input.tagName === "SELECT") {
const selectEl = input as HTMLSelectElement;
value = selectEl.value || undefined;
} else if (input.classList.contains("dtb-multiselect-container")) {
// 多选
const checkboxes = input.querySelectorAll(
'input[type="checkbox"]:checked'
) as NodeListOf<HTMLInputElement>;
const selectedValues = Array.from(checkboxes).map((cb) => cb.value);
value = selectedValues.length > 0 ? selectedValues : undefined;
}
if (value !== undefined) {
// 使用转换函数处理值
const descriptor = descriptorMap.get(key);
if (descriptor?.toApiValue) {
value = descriptor.toApiValue(value);
}
params[key] = value;
}
});
// 解析额外参数JSON
if (this.extraParamsTextarea.value.trim()) {
try {
const extraParams = JSON.parse(this.extraParamsTextarea.value);
Object.assign(params, extraParams);
} catch (error) {
new Notice("Invalid JSON in extra parameters");
}
}
// 收集headers
const headers: Record<string, string> = {};
this.headerInputs.forEach(({ key, value }) => {
if (key.value.trim() && value.value.trim()) {
headers[key.value.trim()] = value.value.trim();
}
});
// 收集自定义设置
const customSettings: Record<string, string> = {};
this.customSettingsInputs.forEach((input, key) => {
if (input.value.trim()) {
customSettings[key] = input.value.trim();
}
});
return {
id: this.apiConfig.id || `api-${Date.now()}`,
name: this.nameInput.value || "Unnamed API",
description: this.descInput.value || "",
type: this.typeSelect.value as WallpaperApiType,
baseUrl: this.urlInput.value,
enabled: this.apiConfig.enabled ?? false, // 默认不启用
params,
headers: Object.keys(headers).length > 0 ? headers : undefined,
customSettings: Object.keys(customSettings).length > 0 ? customSettings : undefined,
};
}
validateApiConfig(config: WallpaperApiConfig): boolean {
// 基本验证
if (!config.name.trim()) {
new Notice("Please enter an API name");
return false;
}
// 调用 API 注册器验证参数
const validation = apiRegistry.validateParams(config.type, config.params);
if (!validation.valid) {
new Notice("Invalid API parameters: " + validation.errors);
return false;
}
return true;
}
// 保存API配置
saveApiConfig() {
const config = this.buildApiConfig();
if (!this.validateApiConfig(config)) {
return;
}
this.onSubmit(config);
this.close();
}
// 测试API配置
async testApiConfig() {
try {
new Notice("Testing API configuration...");
const config = this.buildApiConfig();
if (!this.validateApiConfig(config)) {
new Notice("Cannot test due to invalid configuration");
return;
}
// 这里需临时创建个实例来测试
await apiManager.createApi(config);
// 如果能成功启用则测试通过
await apiManager.enableApi(config.id);
new Notice("API test successful!");
apiManager.deleteApi(config.id); // 测试后删除临时实例
} catch (error) {
new Notice(`API test failed: ${(error as Error).message}`);
}
}
onClose() {
const { contentEl } = this;
contentEl.empty();
}
}

484
src/plugin.ts Normal file
View file

@ -0,0 +1,484 @@
/**
* -
*/
import { Plugin } from "obsidian";
import { registerCommands } from "./commands";
import { getDefaultSettings } from "./default-settings";
import { DTBSettingTab, DTBSettingsView, DTB_SETTINGS_VIEW_TYPE } from "./settings";
import type { BackgroundItem, DTBSettings, TimeRule } from "./types";
import { hexToRgba } from "./utils";
import { apiManager } from "./wallpaper-apis";
export default class DynamicThemeBackgroundPlugin extends Plugin {
settings: DTBSettings;
intervalId: number | null = null;
styleTag: HTMLStyleElement;
// 当前的背景
background: BackgroundItem | null = null;
// ============================================================================
// 主要接口方法
// ============================================================================
async onload() {
await this.loadSettings();
// 创建样式元素
this.styleTag = document.createElement("style");
this.styleTag.id = "dtb-dynamic-styles";
document.head.appendChild(this.styleTag);
// 注册自定义视图类型
this.registerView(DTB_SETTINGS_VIEW_TYPE, (leaf) => new DTBSettingsView(leaf, this));
// 添加设置面板
this.addSettingTab(new DTBSettingTab(this.app, this));
// 注册命令
registerCommands(this);
// 启动背景管理器
if (this.settings.enabled) {
// 等待 layout-ready 事件,确保 vault 完全加载
this.app.workspace.onLayoutReady(() => {
this.startBackgroundManager();
});
}
// Wallpaper API 管理器
// 实例化所有已配置的API
for (const apiConfig of this.settings.wallpaperApis) {
apiManager.createApi(apiConfig);
}
console.log("Dynamic Theme Background plugin loaded");
}
onunload() {
this.stopBackgroundManager();
this.deactivateView(); // 清理自定义视图
this.styleTag?.remove();
// 清理所有注册的API实例包括状态管理器中的所有订阅
apiManager.deleteAllApis();
console.log("Dynamic Theme Background plugin unloaded");
}
async loadSettings() {
const defaultSettings = getDefaultSettings();
this.settings = Object.assign({}, defaultSettings, await this.loadData());
}
async saveSettings() {
await this.saveData(this.settings);
}
/**
*
*/
deactivateView() {
this.app.workspace.detachLeavesOfType(DTB_SETTINGS_VIEW_TYPE);
}
/**
*
*/
async activateView() {
this.deactivateView();
const leaf = this.app.workspace.getLeaf("tab");
await leaf.setViewState({
type: DTB_SETTINGS_VIEW_TYPE,
active: true,
});
// 确保标签页获得焦点
this.app.workspace.revealLeaf(leaf);
}
/**
*
*/
stopBackgroundManager() {
if (this.intervalId) {
clearInterval(this.intervalId);
this.intervalId = null;
}
document.body.classList.remove("dtb-enabled");
console.log("DTB: Background manager stopped");
}
/**
*
*/
startBackgroundManager() {
this.stopBackgroundManager();
// 如果没有 'dtb-enabled' 类,则添加
if (!document.body.classList.contains("dtb-enabled")) {
document.body.classList.add("dtb-enabled");
}
// 立即执行一次更新
this.updateBackground();
// 设置定时器
const intervalMs =
this.settings.mode === "time-based"
? 60000 // 每分钟检查一次
: this.settings.intervalMinutes * 60000;
this.intervalId = this.registerInterval(
window.setInterval(async () => {
await this.updateBackground(false);
}, intervalMs)
);
console.log("DTB: Background manager started", {
mode: this.settings.mode,
interval: intervalMs / 1000 + "s",
});
}
/**
*
* @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) {
this.background = this.settings.backgrounds.find((bg) => bg.id === rule.backgroundId) || null;
// 判断是否与当前背景不同
needsUpdate = this.background?.id !== rule.backgroundId;
// 调试信息, 降低等级,避免刷屏
console.debug("DTB: TimeRule mode - current time rule", rule);
}
break;
}
// 基于时间间隔切换
case "interval": {
// 检查是否启用随机壁纸
if (this.settings.enableRandomWallpaper) {
const randomWallpaperUrl = await this.fetchRandomWallpaper();
if (randomWallpaperUrl) {
// 创建一个临时的背景项用于随机壁纸
this.background = {
id: `random-wallpaper-${Date.now()}`,
name: `Random Wallpaper`,
type: "image",
value: randomWallpaperUrl,
};
needsUpdate = true;
console.debug("DTB: Interval mode - using random wallpaper", randomWallpaperUrl);
} 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];
this.saveSettings();
needsUpdate = true;
console.debug("DTB: Interval mode - fallback to local background", this.background);
}
} else if (this.settings.backgrounds.length > 0) {
// 使用本地背景
this.background = this.settings.backgrounds[this.settings.currentIndex];
this.settings.currentIndex = (this.settings.currentIndex + 1) % this.settings.backgrounds.length;
this.saveSettings();
needsUpdate = true; // 每次间隔切换都需要更新背景
console.debug(
"DTB: Interval mode - current index and background",
this.settings.currentIndex,
this.background
);
}
break;
}
default: {
// 手动模式
this.background = this.settings.backgrounds[this.settings.currentIndex];
}
}
if (forceUpdate || (needsUpdate && this.background)) {
this.updateStyleCss();
}
}
/**
*
*/
getCurrentTimeRule(): TimeRule | null {
if (this.settings.mode !== "time-based") return null;
const now = new Date();
const currentTime = now.getHours() * 60 + now.getMinutes();
for (const rule of this.settings.timeRules) {
if (!rule.enabled) continue;
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;
// 处理跨天的情况如22:00-06:00
if (startTime > endTime) {
if (currentTime >= startTime || currentTime < endTime) {
return rule;
}
} else {
if (currentTime >= startTime && currentTime < endTime) {
return rule;
}
}
}
return null;
}
/**
* CSS, this.background
* @param customBackgroundSize
*/
updateStyleCss(customBackgroundSize?: string) {
if (!this.settings.enabled || !this.background) {
console.warn("DTB: Background update is disabled or no background is set");
return;
}
let cssValue = "";
let backgroundProperty = "";
switch (this.background.type) {
case "image":
cssValue = this.sanitizeImagePath(this.background.value);
backgroundProperty = "background-image";
break;
case "color":
cssValue = this.background.value;
backgroundProperty = "background";
break;
case "gradient":
cssValue = this.background.value;
backgroundProperty = "background";
break;
}
const bgColorWithOpacity = hexToRgba(this.settings.bgColor, this.settings.bgColorOpacity);
// 确定背景尺寸:优先使用传入的自定义尺寸,否则自动计算
let backgroundSize = customBackgroundSize;
if (!backgroundSize) {
if (this.background.type === "image") {
backgroundSize = this.getOptimalBackgroundSize(this.background.value);
} else {
backgroundSize = "auto";
}
}
// TODO 样式还需要优化
this.styleTag.innerText = `
.dtb-enabled :not(.modal):not(.modal *) {
--background-primary: ${bgColorWithOpacity} !important;
--background-primary-alt: ${bgColorWithOpacity} !important;
--background-secondary: ${bgColorWithOpacity} !important;
--background-secondary-alt: ${bgColorWithOpacity} !important;
}
.dtb-enabled .workspace::before {
${backgroundProperty}: ${cssValue} !important;
background-size: ${backgroundSize};
background-repeat: no-repeat;
background-position: center;
filter: blur(${this.settings.blurDepth}px) brightness(${this.settings.brightness4Bg}) saturate(${this.settings.saturate4Bg});
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: -1;
}
`
.trim()
.replace(/[\r\n\s]+/g, " ");
// 通知 css-change
this.app.workspace.trigger("css-change", {
source: "dtb",
});
}
/**
* 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";
}
// 异步加载图片并更新尺寸
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
async fetchRandomWallpaper(): Promise<string | null> {
if (!this.settings.enableRandomWallpaper) {
return null;
}
try {
// 获取所有启用的API配置
const enabledApis = this.settings.wallpaperApis.filter((api) => api.enabled);
// 如果没有启用的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];
console.debug(`DTB: Selected API: ${selectedApi.name} (${selectedApi.type})`);
// 使用选中的API获取壁纸
const wallpaperImages = await apiManager.getRandomWallpapers();
if (!wallpaperImages || wallpaperImages.length === 0) {
console.warn(`DTB: No images returned from API: ${selectedApi.name}`);
return null;
}
const randomImage = wallpaperImages[Math.floor(Math.random() * wallpaperImages.length)];
if (randomImage && randomImage.url) {
console.debug("DTB: Fetched random wallpaper:", randomImage.url);
return randomImage.url;
} else {
console.warn(`DTB: No wallpaper image returned from API: ${selectedApi.name}`);
return null;
}
} catch (error) {
console.error("DTB: Error fetching random wallpaper:", error);
return null;
}
}
// ============================================================================
// 辅助方法
// ============================================================================
// 将图片路径转换为可用的 CSS URL
sanitizeImagePath(imagePath: string): string {
// 判断是否是远程图片
if (imagePath.startsWith("http://") || imagePath.startsWith("https://")) {
return `url(${imagePath})`;
}
// 本地图片路径(只接受 Vault 内的图片)
const file = this.app.vault.getFileByPath(imagePath);
if (!file) {
console.warn(`DTB: Image ${imagePath} not found`);
return "none";
}
const p = this.app.vault.getResourcePath(file);
if (!p) {
console.warn(`DTB: Cannot get resource path for image ${imagePath}`);
return "none";
}
console.debug(`DTB: Using resource path ${p} for image ${imagePath}`);
return `url(${p})`; // 形如 app://local/path/to/image.jpg
}
}

7
src/settings/index.ts Normal file
View file

@ -0,0 +1,7 @@
/**
* -
*
*/
export { DTBSettingTab } from "./settings-tab";
export { DTBSettingsView, DTB_SETTINGS_VIEW_TYPE } from "./settings-view";

1124
src/settings/settings-tab.ts Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,61 @@
/*
*
*/
import { ItemView, WorkspaceLeaf } from "obsidian";
import { t } from "../i18n";
import type DynamicThemeBackgroundPlugin from "../plugin";
import { DTBSettingTab } from "./settings-tab";
export const DTB_SETTINGS_VIEW_TYPE = "dtb-settings";
export class DTBSettingsView extends ItemView {
plugin: DynamicThemeBackgroundPlugin;
settingTab: DTBSettingTab | null;
constructor(leaf: WorkspaceLeaf, plugin: DynamicThemeBackgroundPlugin) {
super(leaf);
this.plugin = plugin;
// 创建一个设置标签页实例,但不是真正的设置标签页
this.settingTab = new DTBSettingTab(this.app, plugin);
}
getViewType(): string {
return DTB_SETTINGS_VIEW_TYPE;
}
getDisplayText(): string {
return t("settings_title");
}
getIcon(): string {
return "settings";
}
async onOpen(): Promise<void> {
const container = this.containerEl.children[1];
container.empty();
// 添加标题
container.createEl("h1", { text: t("settings_title") });
// 使用设置标签页的显示逻辑,但在我们自己的容器中
if (this.settingTab) {
this.settingTab.containerEl = container as HTMLElement;
this.settingTab.display();
}
}
async onClose(): Promise<void> {
// 清理设置标签页的订阅
if (this.settingTab) {
this.settingTab.cleanup();
}
// 清理资源
if (this.settingTab && this.settingTab.containerEl) {
this.settingTab.containerEl.empty();
}
this.settingTab = null; // 释放引用,帮助垃圾回收
this.plugin.deactivateView(); // 确保视图被正确清理
}
}

45
src/types.ts Normal file
View file

@ -0,0 +1,45 @@
/**
* - 使TypeScript接口和类型
* API配置
*/
import type { WallpaperApiConfig } from "./wallpaper-apis";
export interface DTBSettings {
enabled: boolean;
// 统一的背景模糊度、亮度和饱和度变量、背景颜色
blurDepth: number; // 默认模糊度
brightness4Bg: number; // 默认亮度
saturate4Bg: number; // 默认饱和度
bgColor: string; // 默认背景颜色
bgColorOpacity: number; // 默认背景颜色透明度
mode: "time-based" | "interval" | "manual";
timeRules: TimeRule[];
intervalMinutes: number;
backgrounds: BackgroundItem[];
currentIndex: number; // 当前背景索引
// 随机壁纸设置
enableRandomWallpaper: boolean; // 是否启用随机壁纸,否则会顺序使用背景列表中的壁纸
wallpaperApis: WallpaperApiConfig[]; // 壁纸API配置列表
apiRefreshInterval?: number; // API刷新间隔分钟
}
export interface TimeRule {
id: string;
name: string;
startTime: string; // "HH:MM" format
endTime: string; // "HH:MM" format
backgroundId: string;
enabled: boolean;
}
export interface BackgroundItem {
id: string;
name: string;
type: "image" | "color" | "gradient";
value: string; // image URL, color code, or gradient CSS
preview?: string;
}

38
src/utils.ts Normal file
View file

@ -0,0 +1,38 @@
/**
* @file src/utils.ts
* @description Utility functions for the Obsidian Dynamic Theme Background plugin.
*/
/**
* Converts a hexadecimal color string to an RGBA color string with the specified opacity.
*
* Supports both 3-digit and 6-digit hex color formats. If the input is invalid,
* returns a default RGBA color (`rgba(31, 30, 30, opacity)`).
*
* @param hex - The hexadecimal color string (e.g., "#fff" or "#ffffff").
* @param opacity - The opacity value for the RGBA color (between 0 and 1).
* @returns The RGBA color string representation.
*/
export function hexToRgba(hex: string, opacity: number): string {
// 移除 # 符号
hex = hex.replace("#", "");
// 处理3位和6位十六进制颜色
if (hex.length === 3) {
hex = hex
.split("")
.map((char) => char + char)
.join("");
}
if (hex.length !== 6) {
console.warn("DTB: Invalid hex color format:", hex);
return `rgba(31, 30, 30, ${opacity})`;
}
const r = parseInt(hex.substring(0, 2), 16);
const g = parseInt(hex.substring(2, 4), 16);
const b = parseInt(hex.substring(4, 6), 16);
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
}

View file

@ -0,0 +1,44 @@
/**
* API操作错误类型
*/
export enum ApiErrorType {
NETWORK_ERROR = "network_error",
AUTHENTICATION_ERROR = "auth_error",
RATE_LIMIT_ERROR = "rate_limit_error",
CONFIGURATION_ERROR = "config_error",
PARAMETER_ERROR = "param_error",
UNKNOWN_ERROR = "unknown_error",
}
/**
* API操作错误类
*/
export class ApiError extends Error {
constructor(
public type: ApiErrorType,
message: string,
public apiId: string,
public details?: Record<string, unknown>
) {
super(message);
this.name = "ApiError";
}
/**
*
*/
getUserMessage(): string {
switch (this.type) {
case ApiErrorType.NETWORK_ERROR:
return `Network error when connecting to API. Please check your internet connection.`;
case ApiErrorType.AUTHENTICATION_ERROR:
return `Authentication failed. Please check your API credentials.`;
case ApiErrorType.RATE_LIMIT_ERROR:
return `Rate limit exceeded. Please try again later.`;
case ApiErrorType.CONFIGURATION_ERROR:
return `API configuration error. Please check your settings.`;
default:
return `An unexpected error occurred: ${this.message}`;
}
}
}

View file

@ -0,0 +1,269 @@
import { ApiError, ApiErrorType } from "./api-error";
import { apiRegistry } from "./api-registry";
import { ApiStateManager } from "./api-state-manager";
import { BaseWallpaperApi } from "./base-api";
import type { WallpaperApiConfig, WallpaperImage } from "./types";
/**
* API管理器
*
* API实例
*/
class WallpaperApiManager {
// 单例模式
private static instance: WallpaperApiManager;
// API状态管理器
stateManager = new ApiStateManager();
// 存储所有API实例
private apis = new Map<string, BaseWallpaperApi>();
private constructor() {}
static getInstance(): WallpaperApiManager {
if (!WallpaperApiManager.instance) {
WallpaperApiManager.instance = new WallpaperApiManager();
}
return WallpaperApiManager.instance;
}
// ============================================================================
// 主要接口
// ============================================================================
/**
* API实例API启用
* @param config API配置
*/
async createApi(config: WallpaperApiConfig): Promise<void> {
// 验证参数
const validation = apiRegistry.validateParams(config.type, config.params);
if (!validation.valid) {
const error = new ApiError(
ApiErrorType.PARAMETER_ERROR,
`Invalid parameters for API "${config.name}": ${validation.errors?.join(", ") || "Unknown error"}`,
config.id
);
console.warn(`WallpaperApiManager: ${error.message}`);
return;
}
// 使用注册表动态创建API实例
const ApiClass = apiRegistry.getApiClass(config.type);
if (!ApiClass) {
console.warn(`WallpaperApiManager: Unsupported API type "${config.type}". No registered API class found.`);
return;
}
// 如果没有提供ID则生成一个新的唯一标识符
config.id ||= this.genApiId();
const api = new ApiClass(config);
this.apis.set(api.getId(), api);
// 如果配置中启用了API则异步启用实例
if (config.enabled) {
await this.enableApi(api.getId());
}
}
/**
* API实例
* @param apiId API实例唯一标识符
*/
deleteApi(apiId: string): void {
this.apis.delete(apiId);
// 清理状态管理器中的订阅
this.stateManager.cleanupByApiId(apiId);
}
/**
* API实例
*/
deleteAllApis(): void {
this.apis.clear();
// 清理状态管理器中的所有订阅
this.stateManager.cleanup();
}
/**
* ID获取API实例
* @param apiId API实例唯一标识符
*/
getApiById(apiId: string): BaseWallpaperApi | null {
return this.apis.get(apiId) || null;
}
/**
* API
*/
getAllApis(): BaseWallpaperApi[] {
return Array.from(this.apis.values());
}
/**
* API名称
*/
getApiNames(): string[] {
return Array.from(this.apis.values()).map((api) => api.getName());
}
/**
* API - 退
* @param apiId API实例唯一标识符
* @returns Promise<boolean> truefalse
*/
async enableApi(apiId: string): Promise<boolean> {
// 1. 先找API实例
const api = this.getApiById(apiId);
if (!api) {
const error = new ApiError(
ApiErrorType.CONFIGURATION_ERROR,
`API instance with ID "${apiId}" not found`,
apiId
);
console.warn(`WallpaperApiManager: ${error.message}`);
this.stateManager.notify(apiId, {
configEnabled: false,
instanceEnabled: false,
isLoading: false,
error: error.getUserMessage(),
});
return false;
}
const apiConfig = api.getConfig();
// 2. 状态管理器通知订阅者 api 处于加载状态
this.stateManager.notify(apiId, {
configEnabled: true,
instanceEnabled: false,
isLoading: true,
});
// 3. 异步尝试启用API实例
try {
const success = await api.tryEnable();
if (!success) {
// 4. 更新状态管理器
this.stateManager.notify(apiId, {
configEnabled: false,
instanceEnabled: false,
isLoading: false,
error: `Failed to enable API "${apiConfig.name}"`,
});
apiConfig.enabled = false;
console.warn(`WallpaperApiManager: Failed to enable API "${apiId}", config reverted.`);
return false;
}
// 5. 成功时更新状态
this.stateManager.notify(apiId, {
configEnabled: true,
instanceEnabled: true,
isLoading: false,
});
apiConfig.enabled = true;
return true;
} catch (error) {
// 处理不同类型的错误
let apiError: ApiError;
if (error instanceof ApiError) {
apiError = error;
} else {
apiError = new ApiError(
ApiErrorType.UNKNOWN_ERROR,
error instanceof Error ? error.message : String(error),
apiId
);
}
// 6. 更新状态管理器
this.stateManager.notify(apiId, {
configEnabled: false,
instanceEnabled: false,
isLoading: false,
error: apiError.getUserMessage(),
});
apiConfig.enabled = false;
console.error(`WallpaperApiManager: Error enabling API "${apiId}":`, error);
return false;
}
}
/**
* API
* @param apiId API实例唯一标识符
* @returns Promise<boolean> truefalse
*/
async disableApi(apiId: string): Promise<boolean> {
// 1. 先找到配置和API实例
const api = this.getApiById(apiId);
if (!api) {
console.warn(`WallpaperApiManager: API config or instance with ID "${apiId}" not found.`);
return false;
}
// 不考虑禁用失败回退那一套,因为还可能涉及恢复启用状态之类的逻辑,不搞这么复杂。。。
await api.tryDisable();
this.stateManager.notify(apiId, {
configEnabled: false,
instanceEnabled: false,
isLoading: false,
});
return true;
}
/**
* API API
*
* @param apiId 使 API ID API
* @param count 1
* @returns WallpaperImage[] API null
*/
async getRandomWallpapers(apiId?: string, count = 1): Promise<WallpaperImage[] | null> {
let randomApi: BaseWallpaperApi;
// 如果指定了API ID则使用该API获取壁纸
if (apiId) {
const foundApi = this.getApiById(apiId);
if (!foundApi) {
console.warn(`WallpaperApiManager: API with ID "${apiId}" not found.`);
return null;
}
randomApi = foundApi;
} else {
// 随机从已启用的API中选择一个
const enabledApis = Array.from(this.apis.values()).filter((api) => api.getEnabled());
if (enabledApis.length === 0) {
console.warn("No enabled wallpaper APIs available.");
return null;
}
randomApi = enabledApis[Math.floor(Math.random() * enabledApis.length)];
}
// 获取壁纸
const imageList = await randomApi.getImages(count);
if (!imageList || imageList.length === 0) {
console.warn(`No images found for API: ${randomApi.getName()}`);
return null;
}
return imageList;
}
// ============================================================================
// 辅助方法
// ============================================================================
private genApiId(): string {
return `api-${crypto.randomUUID()}`;
}
}
// 单例模式
export const apiManager = WallpaperApiManager.getInstance();

View file

@ -0,0 +1,151 @@
/**
* API
* 使
*/
import type { BaseWallpaperApi } from "./base-api";
import type {
WallpaperApiConfig,
WallpaperApiEndpoints,
WallpaperApiParamDescriptor,
WallpaperApiParams,
WallpaperApiType,
} from "./types";
// 简化的API类类型定义 - 包含构造函数和静态方法
interface WallpaperApiClass {
new (config: WallpaperApiConfig): BaseWallpaperApi;
getDefaultBaseUrl(): string;
getDefaultEndpoints(): WallpaperApiEndpoints;
getDefaultDescription(): string;
getApiDocUrl(): string;
getTokenUrl(): string;
getDefaultParams(): WallpaperApiParams;
getParamDescriptors(): WallpaperApiParamDescriptor[];
validateParams(params: WallpaperApiParams): { valid: boolean; errors?: string[] };
}
/**
* API
*/
class ApiRegistry {
private static instance: ApiRegistry;
private apiClasses = new Map<WallpaperApiType, WallpaperApiClass>();
private constructor() {}
static getInstance(): ApiRegistry {
if (!ApiRegistry.instance) {
ApiRegistry.instance = new ApiRegistry();
}
return ApiRegistry.instance;
}
/**
* API
* TypeScript会自动检查继承关系
*/
register(type: WallpaperApiType, apiClass: WallpaperApiClass): void {
this.apiClasses.set(type, apiClass);
console.log(`✅ API class registered: ${apiClass.name} (${type})`);
}
/**
* API
*/
getApiClass(type: WallpaperApiType): WallpaperApiClass | undefined {
return this.apiClasses.get(type);
}
/**
* API
*/
getRegisteredTypes(): WallpaperApiType[] {
return Array.from(this.apiClasses.keys());
}
/**
* API
*/
createInstance(type: WallpaperApiType, config: WallpaperApiConfig): BaseWallpaperApi | null {
const ApiClass = this.apiClasses.get(type);
if (!ApiClass) {
console.warn(`API type "${type}" is not registered`);
return null;
}
return new ApiClass(config);
}
// ============================================================================
// api 静态方法
// ============================================================================
/**
* URL
*/
getDefaultBaseUrl(type: WallpaperApiType): string | null {
const ApiClass = this.apiClasses.get(type);
return ApiClass ? ApiClass.getDefaultBaseUrl() : null;
}
/**
*
*/
getDefaultEndpoints(type: WallpaperApiType): WallpaperApiEndpoints | null {
const ApiClass = this.apiClasses.get(type);
return ApiClass ? ApiClass.getDefaultEndpoints() : null;
}
/**
*
*/
getDefaultDescription(type: WallpaperApiType): string | null {
const ApiClass = this.apiClasses.get(type);
return ApiClass ? ApiClass.getDefaultDescription() : null;
}
/**
* API文档URL
*/
getApiDocUrl(type: WallpaperApiType): string | null {
const ApiClass = this.apiClasses.get(type);
return ApiClass ? ApiClass.getApiDocUrl() : null;
}
/**
* Token URL
*/
getTokenUrl(type: WallpaperApiType): string | null {
const ApiClass = this.apiClasses.get(type);
return ApiClass ? ApiClass.getTokenUrl() : null;
}
/**
*
*/
getDefaultParams(type: WallpaperApiType): WallpaperApiParams {
const ApiClass = this.apiClasses.get(type);
return ApiClass ? ApiClass.getDefaultParams() : {};
}
/**
*
*/
getParamDescriptors(type: WallpaperApiType): WallpaperApiParamDescriptor[] {
const ApiClass = this.apiClasses.get(type);
return ApiClass ? ApiClass.getParamDescriptors() : [];
}
/**
*
*/
validateParams(type: WallpaperApiType, params: WallpaperApiParams): { valid: boolean; errors?: string[] } {
const ApiClass = this.apiClasses.get(type);
return ApiClass ? ApiClass.validateParams(params) : { valid: false, errors: ["API type not registered"] };
}
}
/**
*
*/
export const apiRegistry = ApiRegistry.getInstance();

View file

@ -0,0 +1,218 @@
/**
* API状态管理器
* API的配置状态UI状态之间的同步
*/
/**
* API状态订阅者
*/
export class ApiStateSubscriber {
name: string;
componentId: string;
apiId: string;
constructor(name: string, componentId: string, apiId: string) {
this.name = name;
this.componentId = componentId;
this.apiId = apiId;
}
/**
*
*/
key(): string {
return `${this.apiId}::${this.componentId}::${this.name}`;
}
}
export class ApiStateManager {
private listeners: Map<
string,
{ subscriber: ApiStateSubscriber; callback: (state: ApiState) => void | Promise<void> }
> = new Map();
/**
* API状态变化
* @param subscriber
* @param callback
*/
subscribe(subscriber: ApiStateSubscriber, callback: (state: ApiState) => void | Promise<void>): () => void {
const key = subscriber.key();
this.listeners.set(key, { subscriber, callback });
return () => {
this.listeners.delete(key);
};
}
/**
*
* @param apiId API ID
* @param state
*/
notify(apiId: string, state: ApiState): void {
// 立即返回,异步执行所有回调以避免阻塞调用者
setTimeout(async () => {
const callbackPromises: Promise<void>[] = [];
for (const { subscriber, callback } of this.listeners.values()) {
if (subscriber.apiId === apiId) {
// 创建一个 Promise 来处理每个回调(无论同步还是异步)
const callbackPromise = Promise.resolve().then(async () => {
try {
const result = callback(state);
// 如果是 Promise等待完成
if (result && typeof result.then === "function") {
await result;
}
} catch (error) {
console.warn(`DTB: Error in state change callback for ${subscriber.name}:`, error);
}
});
callbackPromises.push(callbackPromise);
}
}
// 并发执行所有回调,但不等待结果(火后即忘模式)
if (callbackPromises.length > 0) {
Promise.all(callbackPromises).catch((error) => {
console.warn("DTB: Unexpected error in state notification batch:", error);
});
}
}, 0);
}
/**
*
* @param subscriber
*/
cleanupBySubscriber(subscriber: ApiStateSubscriber): void {
const key = subscriber.key();
this.listeners.delete(key);
}
/**
* ID清理订阅
* @param componentId ID
*/
cleanupByComponent(componentId: string): void {
const keysToDelete: string[] = [];
for (const [key, { subscriber }] of this.listeners.entries()) {
if (subscriber.componentId === componentId) {
keysToDelete.push(key);
}
}
keysToDelete.forEach((key) => this.listeners.delete(key));
}
/**
* API的所有订阅
* @param apiId API ID
*/
cleanupByApiId(apiId: string): void {
const keysToDelete: string[] = [];
for (const [key, { subscriber }] of this.listeners.entries()) {
if (subscriber.apiId === apiId) {
keysToDelete.push(key);
}
}
keysToDelete.forEach((key) => this.listeners.delete(key));
}
/**
*
* @param subscriber
*/
hasSubscription(subscriber: ApiStateSubscriber): boolean {
const key = subscriber.key();
return this.listeners.has(key);
}
/**
*
*/
getStats(): { totalSubscriptions: number; apiCount: number; subscriberCount: number; componentCount: number } {
const apis = new Set<string>();
const components = new Set<string>();
for (const { subscriber } of this.listeners.values()) {
apis.add(subscriber.apiId);
components.add(subscriber.componentId);
}
return {
totalSubscriptions: this.listeners.size,
apiCount: apis.size,
subscriberCount: this.listeners.size, // 每个订阅对应一个订阅者
componentCount: components.size,
};
}
/**
* API的所有订阅者
* @param apiId API ID
*/
getSubscribers(apiId: string): ApiStateSubscriber[] {
const result: ApiStateSubscriber[] = [];
for (const { subscriber } of this.listeners.values()) {
if (subscriber.apiId === apiId) {
result.push(subscriber);
}
}
return result;
}
/**
* ID获取订阅者
* @param componentId ID
*/
getSubscribersByComponent(componentId: string): ApiStateSubscriber[] {
const result: ApiStateSubscriber[] = [];
for (const { subscriber } of this.listeners.values()) {
if (subscriber.componentId === componentId) {
result.push(subscriber);
}
}
return result;
}
/**
*
* @param name
*/
getSubscribersByName(name: string): ApiStateSubscriber[] {
const result: ApiStateSubscriber[] = [];
for (const { subscriber } of this.listeners.values()) {
if (subscriber.name === name) {
result.push(subscriber);
}
}
return result;
}
/**
* API ID
*/
getApiIds(): string[] {
const apis = new Set<string>();
for (const { subscriber } of this.listeners.values()) {
apis.add(subscriber.apiId);
}
return Array.from(apis);
}
/**
*
*/
cleanup(): void {
this.listeners.clear();
}
}
export interface ApiState {
configEnabled: boolean;
instanceEnabled: boolean;
isLoading: boolean;
error?: string;
}

View file

@ -0,0 +1,261 @@
/**
* API基础类
*
*/
import type {
WallpaperApiConfig,
WallpaperApiEndpoints,
WallpaperApiParamDescriptor,
WallpaperApiParams,
WallpaperApiType,
WallpaperImage,
} from "./types";
/**
* API实例的基础类
*
*/
export abstract class BaseWallpaperApi {
// ============================================================================
// 基础属性
// ============================================================================
// 实例标识和配置
protected readonly id: string;
protected readonly name: string;
protected readonly description: string;
protected readonly type: WallpaperApiType;
protected readonly config: WallpaperApiConfig; // API实例配置的引用
// API连接配置
protected readonly baseUrl: string;
protected readonly endpoints: WallpaperApiEndpoints;
protected readonly params: WallpaperApiParams;
// 状态管理
protected enabled = false;
protected initialized = false;
// ============================================================================
// 数据缓存相关
// ============================================================================
protected wallpaperImageCache: WallpaperImage[] = []; // 为避免爆内存,这里应仅缓存当前页的数据
protected curDataIndex = 0;
protected currentPage = 1;
protected totalPages = -1;
protected totalCount = -1;
constructor(config: WallpaperApiConfig) {
this.config = config;
this.id = config.id;
this.name = config.name;
this.type = config.type;
this.description = config.description || (this.constructor as typeof BaseWallpaperApi).getDefaultDescription();
this.baseUrl = config.baseUrl || (this.constructor as typeof BaseWallpaperApi).getDefaultBaseUrl();
this.endpoints = {
...(this.constructor as typeof BaseWallpaperApi).getDefaultEndpoints(),
...config.endpoints,
};
this.params = {
...(this.constructor as typeof BaseWallpaperApi).getDefaultParams(),
...config.params,
};
this.saveConfig();
}
// ============================================================================
// 抽象方法 - 子类必须实现
// ============================================================================
abstract init(): Promise<boolean>; // 启用插件时必须调用
abstract deinit(): Promise<boolean>; // 禁用插件时必须调用
abstract updateImageCache(): Promise<boolean>; // 更新图片缓存数据 wallpaperImageCache
// ============================================================================
// 静态方法 - 基类中抛出错误的方法,子类必须实现
// ============================================================================
static getDefaultBaseUrl(): string {
throw new Error("getDefaultBaseUrl must be implemented by subclass");
}
static getDefaultEndpoints(): WallpaperApiEndpoints {
throw new Error("getDefaultEndpoints must be implemented by subclass");
}
static getDefaultDescription(): string {
return "No description provided.";
}
static getApiDocUrl(): string {
return "";
}
static getTokenUrl(): string {
return "";
}
static getDefaultParams(): WallpaperApiParams {
return {};
}
static getParamDescriptors(): WallpaperApiParamDescriptor[] {
return [];
}
static validateParams(params: WallpaperApiParams): { valid: boolean; errors?: string[] } {
return { valid: true };
}
// ============================================================================
// 基础信息获取方法
// ============================================================================
getId(): string {
return this.id;
}
getName(): string {
return this.name;
}
getDescription(): string {
return this.description;
}
getType(): WallpaperApiType {
return this.type;
}
getConfig(): WallpaperApiConfig {
return this.config;
}
getParams(): WallpaperApiParams {
return { ...this.params };
}
getBaseUrl(): string {
return this.baseUrl;
}
getEndpoints(): WallpaperApiEndpoints {
return { ...this.endpoints };
}
// ============================================================================
// 状态管理方法
// ============================================================================
getEnabled(): boolean {
return this.enabled;
}
async tryEnable(): Promise<boolean> {
if (this.enabled) {
return true;
}
try {
const success = await this.init();
if (success) {
this.enabled = true;
}
return success;
} catch (error) {
console.error(`Failed to enable API "${this.name}":`, error);
return false;
}
}
async tryDisable(): Promise<boolean> {
if (!this.enabled) {
return true;
}
try {
const success = await this.deinit();
if (success) {
this.enabled = false;
}
return success;
} catch (error) {
console.error(`Failed to disable API "${this.name}":`, error);
return false;
}
}
// ============================================================================
// 工具方法
// ============================================================================
protected saveConfig(): void {
// 将 api 的配置同步过到 config 中
this.config.baseUrl = this.baseUrl;
this.config.endpoints = this.endpoints;
this.config.params = this.params;
}
protected buildEndpointUrl(endpointKey: string, pathParams?: Record<string, string>): string {
const endpoint = this.endpoints[endpointKey];
if (!endpoint) {
throw new Error(`Endpoint '${endpointKey}' not found`);
}
let url = `${this.baseUrl}${endpoint}`;
// 替换路径参数 (如: /w/{id} -> /w/123)
if (pathParams) {
Object.entries(pathParams).forEach(([key, value]) => {
url = url.replace(`{${key}}`, encodeURIComponent(value));
});
}
return url;
}
// 辅助方法:构建查询字符串
protected buildQuery(params: WallpaperApiParams = this.params): string {
return Object.entries(params)
.filter(([, value]) => value !== undefined)
.map(([key, value]) => {
const encodedKey = encodeURIComponent(key);
const encodedValue = Array.isArray(value)
? encodeURIComponent(value.join(","))
: encodeURIComponent(String(value));
return `${encodedKey}=${encodedValue}`;
})
.join("&");
}
// ============================================================================
// 核心业务方法
// ============================================================================
async getImages(imageNum = 1): Promise<WallpaperImage[] | null> {
const images: WallpaperImage[] = [];
while (images.length < imageNum) {
const remaining = imageNum - images.length;
const available = this.wallpaperImageCache.length - this.curDataIndex;
if (available > 0) {
const slice = this.wallpaperImageCache.slice(this.curDataIndex, this.curDataIndex + remaining);
images.push(...slice);
this.curDataIndex += slice.length;
}
if (images.length < imageNum) {
await this.updateImageCache();
if (this.wallpaperImageCache.length === 0) {
break; // 没有更多图片了
}
this.curDataIndex = 0;
}
}
return images.length > 0 ? images : null;
}
}

View file

@ -0,0 +1,7 @@
// 导出所有核心API功能
export * from "./api-error";
export * from "./api-manager";
export * from "./api-registry";
export * from "./api-state-manager";
export * from "./base-api";
export * from "./types";

View file

@ -0,0 +1,88 @@
/**
* API核心类型定义
*/
/**
* API
*/
export enum WallpaperApiType {
Unsplash = "Unsplash",
Pexels = "Pexels",
Pixabay = "Pixabay",
Wallhaven = "Wallhaven",
Custom = "Custom",
}
/**
* API
*/
export interface WallpaperApiParamDescriptor {
key: string; // 参数key
label: string; // 显示标签
type: "string" | "number" | "boolean" | "select" | "multiselect" | "password"; // 参数类型
required?: boolean; // 是否必填
defaultValue?: string | number | boolean | string[]; // 默认值(注: 提供的值应为 api 参数值
placeholder?: string; // 占位符
description?: string; // 参数描述
options?: { value: string | number; label: string }[]; // 选择项用于select和multiselect
// 注界面值为便于用户理解的值API参数值为实际传递给API的值
// 转换函数将界面值转换为API参数值
toApiValue?: (uiValue: string | number | boolean | string[]) => string | number | boolean | string[];
// 转换函数将API参数值转换为界面值
fromApiValue?: (apiValue: string | number | boolean | string[]) => string | number | boolean | string[];
}
/**
* API
*/
export interface WallpaperApiEndpoints {
search?: string; // 搜索端点
detail?: string; // 获取详细信息端点
download?: string; // 下载端点
[key: string]: string | undefined; // 允许自定义端点
}
/**
* API
* API实现类定义自己需要的参数
*/
export interface WallpaperApiParams {
[key: string]: string | number | boolean | string[] | undefined;
}
/**
*
*/
export interface WallpaperImage {
url: string;
id: string;
author?: string;
description?: string;
tags?: string[];
width?: number;
height?: number;
downloadUrl?: string;
}
/**
* API实例配置接口
*/
export interface WallpaperApiConfig {
id: string; // API实例唯一标识符, 由插件自动生成,用户不应修改
type: WallpaperApiType;
enabled: boolean; // 记录该API是否启用
name: string;
description?: string; // API实例描述
baseUrl: string; // API基础域名或服务地址 (如: https://wallhaven.cc/api/v1)
endpoints?: WallpaperApiEndpoints; // 具体的端点配置,如果不提供则使用默认端点
headers?: Record<string, string>; // 请求头
params: WallpaperApiParams;
// 自定义设置
customSettings?: {
imageUrlJsonPath?: string; // 从响应中提取图片URL的JSON路径
[key: string]: string | undefined; // 允许其他自定义设置
};
}

View file

@ -0,0 +1,3 @@
// 导出所有API相关的类和接口
export * from "./core";
export * from "./providers";

View file

@ -0,0 +1,2 @@
// 导出所有API提供商实现
export * from "./wallhaven";

View file

@ -0,0 +1,388 @@
/* Wallhaven API
* 文档: https://wallhaven.cc/help/api
*/
import { requestUrl } from "obsidian";
import {
apiRegistry,
BaseWallpaperApi,
WallpaperApiEndpoints,
WallpaperApiParamDescriptor,
WallpaperApiParams,
WallpaperApiType,
WallpaperImage,
} from "../core";
export class WallhavenApi extends BaseWallpaperApi {
type: WallpaperApiType = WallpaperApiType.Wallhaven;
perPage = 24; // 官方设定为24不可更改
// ============================================================================
// 静态方法 - 用于UI显示默认值
// ============================================================================
static getDefaultBaseUrl(): string {
return "https://wallhaven.cc/api/v1";
}
static getDefaultEndpoints(): WallpaperApiEndpoints {
return {
search: "/search",
detail: "/w/{id}",
tag: "/search/tags",
};
}
static getDefaultDescription(): string {
return "Wallhaven API for fetching wallpapers. Supports SFW, sketchy, and NSFW content. Categories include general, anime, and people.";
}
static getApiDocUrl(): string {
return "https://wallhaven.cc/help/api";
}
static getTokenUrl(): string {
return "https://wallhaven.cc/settings/account";
}
static getDefaultParams(): WallpaperApiParams {
return {
categories: "111", // general, anime, people
purity: "100", // SFW only
sorting: "random",
order: "desc",
topRange: "1M", // Last 1 month
page: 1,
};
}
static getParamDescriptors(): WallpaperApiParamDescriptor[] {
const defaultParams = WallhavenApi.getDefaultParams();
return [
{
key: "apikey",
label: "API Key",
type: "password",
required: false,
placeholder: "Your Wallhaven API key (optional)",
description: "Required for NSFW content and higher rate limits",
},
{
key: "q",
label: "Search Query",
type: "string",
required: false,
placeholder: "nature, landscape, abstract...",
description: "Search keywords for wallpapers",
},
{
key: "categories",
label: "Categories",
type: "multiselect",
defaultValue: "111",
description: "Select which categories to include",
options: [
{ value: "general", label: "General" },
{ value: "anime", label: "Anime" },
{ value: "people", label: "People" },
],
toApiValue: (uiValue: string | number | boolean | string[]) => {
if (!Array.isArray(uiValue)) return defaultParams.categories as string;
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);
},
fromApiValue: (apiValue: string | number | boolean | string[]) => {
const str = apiValue?.toString() || (defaultParams.categories as string);
const result: string[] = [];
if (str[0] === "1") result.push("general");
if (str[1] === "1") result.push("anime");
if (str[2] === "1") result.push("people");
return result;
},
},
{
key: "purity",
label: "Content Purity",
type: "multiselect",
defaultValue: "100",
description: "Select content purity levels",
options: [
{ value: "sfw", label: "SFW (Safe for Work)" },
{ value: "sketchy", label: "Sketchy" },
{ value: "nsfw", label: "NSFW (18+)" },
],
toApiValue: (uiValue: string | number | boolean | string[]) => {
if (!Array.isArray(uiValue)) return defaultParams.purity as string;
let result = "";
result += uiValue.includes("sfw") ? "1" : "0";
result += uiValue.includes("sketchy") ? "1" : "0";
result += uiValue.includes("nsfw") ? "1" : "0";
return result || (defaultParams.purity as string);
},
fromApiValue: (apiValue: string | number | boolean | string[]) => {
const str = apiValue?.toString() || (defaultParams.purity as string);
const result: string[] = [];
if (str[0] === "1") result.push("sfw");
if (str[1] === "1") result.push("sketchy");
if (str[2] === "1") result.push("nsfw");
return result;
},
},
{
key: "sorting",
label: "Sort By",
type: "select",
defaultValue: defaultParams.sorting,
description: "How to sort the results",
options: [
{ value: "date_added", label: "Date Added" },
{ value: "relevance", label: "Relevance" },
{ value: "random", label: "Random" },
{ value: "views", label: "Views" },
{ value: "favorites", label: "Favorites" },
{ value: "toplist", label: "Top List" },
],
},
{
key: "order",
label: "Order",
type: "select",
defaultValue: defaultParams.order,
description: "Order of the results",
options: [
{ value: "desc", label: "Descending" },
{ value: "asc", label: "Ascending" },
],
},
{
key: "topRange",
label: "Top Range",
type: "select",
defaultValue: defaultParams.topRange,
description: "Time range for top wallpapers",
options: [
{ value: "1W", label: "Last Week" },
{ value: "1M", label: "Last Month" },
{ value: "3M", label: "Last 3 Months" },
{ value: "6M", label: "Last 6 Months" },
{ value: "1Y", label: "Last Year" },
{ value: "all", label: "All Time" },
],
},
{
key: "atleast",
label: "Minimum Resolution",
type: "string",
required: false,
placeholder: "1920x1080",
description: "Minimum resolution in WIDTHxHEIGHT format",
},
{
key: "resolutions",
label: "Resolutions",
type: "string",
required: false,
placeholder: "1920x1080,2560x1440",
description: "Comma-separated resolutions in WIDTHxHEIGHT format (e.g., 1920x1080,2560x1440)",
},
{
key: "ratios",
label: "Aspect Ratios",
type: "string",
required: false,
placeholder: "16x9,16x10",
description: "Comma-separated aspect ratios (e.g., 16x9,16x10,21x9)",
},
{
key: "colors",
label: "Colors",
type: "string",
required: false,
placeholder: "660000,990000",
description: "Comma-separated hex colors without # (e.g., 660000,990000)",
},
{
key: "page",
label: "Page Number",
type: "number",
defaultValue: defaultParams.page,
description: "Page number to fetch",
},
{
key: "seed",
label: "Seed",
type: "string",
required: false,
placeholder: "[a-zA-Z0-9]{6}",
description: "Optional seed for random results",
},
];
}
static validateParams(params: WallpaperApiParams): { valid: boolean; errors?: string[] } {
let valid = true;
const errors: string[] = [];
// 验证 atleast 是否符合 WIDTHxHEIGHT 格式
if (params.atleast && !/^\d+x\d+$/.test(String(params.atleast))) {
valid = false;
errors.push("Invalid resolution format for 'atleast'. Use WIDTHxHEIGHT.");
}
// 验证 resolutions 是否符合逗号分隔的 WIDTHxHEIGHT 格式
if (params.resolutions) {
const resolutions = String(params.resolutions).split(",");
for (const res of resolutions) {
if (!/^\d+x\d+$/.test(res)) {
valid = false;
errors.push("Invalid resolution format in 'resolutions'. Use WIDTHxHEIGHT.");
break;
}
}
}
// 验证 ratios 是否符合逗号分隔的 WIDTHxHEIGHT 格式
if (params.ratios) {
const ratios = String(params.ratios).split(",");
for (const ratio of ratios) {
if (!/^\d+x\d+$/.test(ratio)) {
valid = false;
errors.push("Invalid aspect ratio format in 'ratios'. Use WIDTHxHEIGHT.");
break;
}
}
}
// 验证 colors 是否符合逗号分隔的十六进制颜色格式
if (params.colors) {
const colors = String(params.colors).split(",");
for (const color of colors) {
if (!/^[0-9a-fA-F]{6}$/.test(color)) {
valid = false;
errors.push("Invalid color format in 'colors'. Use 6-digit hex without #.");
break;
}
}
}
// 验证 seed 是否符合 [a-zA-Z0-9]{6} 格式
if (params.seed && !/^[a-zA-Z0-9]{6}$/.test(String(params.seed))) {
valid = false;
errors.push("Invalid seed format. Use exactly 6 alphanumeric characters.");
}
return { valid: valid, errors: errors.length > 0 ? errors : undefined };
}
// ============================================================================
// 接口方法
// ============================================================================
async init(): Promise<boolean> {
if (this.initialized) {
return true;
}
// 拉取第一页数据用于测试连通性
const resp = await this.fetchSearchResults(1);
if (!resp) {
console.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.");
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.");
return false;
}
// 初始化数据缓存
this.wallpaperImageCache = [];
this.curDataIndex = 0;
this.currentPage = 1;
this.initialized = true;
return true;
}
async deinit(): Promise<boolean> {
if (!this.initialized) {
return true;
}
// 清理缓存数据
this.wallpaperImageCache = [];
this.curDataIndex = 0;
this.currentPage = 1;
this.initialized = false;
return 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> {
try {
const data = await this.fetchSearchResults(page);
if (!data.data || !Array.isArray(data.data)) {
console.warn("Invalid API response format from Wallhaven");
return false;
}
// 为避免爆内存,这里应仅缓存当前页的数据
this.wallpaperImageCache = data.data.map((img: Record<string, unknown>) => this.transformImage(img));
return true;
} catch (error) {
console.warn(`Error fetching images from Wallhaven API:`, error);
return false;
}
}
// 搜索请求, 返回 response.json
private async fetchSearchResults(page = this.currentPage) {
const url = `${this.buildEndpointUrl("search")}?${this.buildQuery({ ...this.params, page })}`;
const response = await requestUrl({ url });
return response.json;
}
// 辅助方法:转换 API 返回的图片数据为 WallpaperImage
private transformImage(apiImage: Record<string, unknown>): WallpaperImage {
return {
id: String(apiImage.id || ""),
url: String(apiImage.path || ""),
width: Number(apiImage.dimension_x) || undefined,
height: Number(apiImage.dimension_y) || undefined,
};
}
}
// 注册 Wallhaven API
apiRegistry.register(WallpaperApiType.Wallhaven, WallhavenApi);

1400
styles.css

File diff suppressed because it is too large Load diff