mirror of
https://github.com/sean2077/obsidian-dynamic-theme-background.git
synced 2026-07-22 06:44:57 +00:00
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:
parent
c6f1d4675f
commit
8c65861080
41 changed files with 5565 additions and 2508 deletions
|
|
@ -2,6 +2,7 @@
|
|||
"trailingComma": "es5",
|
||||
"tabWidth": 4,
|
||||
"semi": true,
|
||||
"printWidth": 120,
|
||||
"plugins": [],
|
||||
"overrides": []
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
|
||||
- **时间段背景**: 根据时间段自动切换背景
|
||||
- **手动切换**: 允许用户手动切换背景
|
||||
- **🆕 随机壁纸支持**: 从流行的API获取随机壁纸(Unsplash、Pixabay、Pexels),支持间隔模式下的自动切换
|
||||
- **自定义设置**: 用户可以自定义背景模糊度、亮度、饱和度和颜色
|
||||
- **支持多种背景类型**: 包括图片、颜色背景和渐变背景,其中图片背景支持远程图片和仓库本地图片
|
||||
- **可与已有主题兼容**: 该插件可以与现有的 Obsidian 主题一起使用(但会覆盖一些样式,详见[注意](#注意))
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
91
lang/en.json
91
lang/en.json
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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": "拖拽以重新排序"
|
||||
}
|
||||
51
src/commands/fetch-wallpaper-command.ts
Normal file
51
src/commands/fetch-wallpaper-command.ts
Normal 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
29
src/commands/index.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
26
src/commands/next-background-command.ts
Normal file
26
src/commands/next-background-command.ts
Normal 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,
|
||||
})
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
16
src/commands/open-settings-command.ts
Normal file
16
src/commands/open-settings-command.ts
Normal 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();
|
||||
},
|
||||
};
|
||||
}
|
||||
46
src/commands/test-background-command.ts
Normal file
46
src/commands/test-background-command.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
25
src/commands/toggle-command.ts
Normal file
25
src/commands/toggle-command.ts
Normal 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
144
src/default-settings.ts
Normal 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
135
src/i18n/en.ts
Normal 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
24
src/i18n/index.ts
Normal 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
131
src/i18n/zh-cn.ts
Normal 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
1
src/main.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./plugin";
|
||||
112
src/modals/background-modal.ts
Normal file
112
src/modals/background-modal.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
32
src/modals/image-folder-suggest-modal.ts
Normal file
32
src/modals/image-folder-suggest-modal.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
40
src/modals/image-path-suggest-modal.ts
Normal file
40
src/modals/image-path-suggest-modal.ts
Normal 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
10
src/modals/index.ts
Normal 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";
|
||||
78
src/modals/time-rule-modal.ts
Normal file
78
src/modals/time-rule-modal.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
641
src/modals/wallpaper-api-modal.ts
Normal file
641
src/modals/wallpaper-api-modal.ts
Normal 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
484
src/plugin.ts
Normal 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
7
src/settings/index.ts
Normal 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
1124
src/settings/settings-tab.ts
Normal file
File diff suppressed because it is too large
Load diff
61
src/settings/settings-view.ts
Normal file
61
src/settings/settings-view.ts
Normal 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
45
src/types.ts
Normal 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
38
src/utils.ts
Normal 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})`;
|
||||
}
|
||||
44
src/wallpaper-apis/core/api-error.ts
Normal file
44
src/wallpaper-apis/core/api-error.ts
Normal 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}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
269
src/wallpaper-apis/core/api-manager.ts
Normal file
269
src/wallpaper-apis/core/api-manager.ts
Normal 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> 成功返回true,失败返回false
|
||||
*/
|
||||
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> 成功返回true,失败返回false
|
||||
*/
|
||||
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();
|
||||
151
src/wallpaper-apis/core/api-registry.ts
Normal file
151
src/wallpaper-apis/core/api-registry.ts
Normal 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();
|
||||
218
src/wallpaper-apis/core/api-state-manager.ts
Normal file
218
src/wallpaper-apis/core/api-state-manager.ts
Normal 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;
|
||||
}
|
||||
261
src/wallpaper-apis/core/base-api.ts
Normal file
261
src/wallpaper-apis/core/base-api.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
7
src/wallpaper-apis/core/index.ts
Normal file
7
src/wallpaper-apis/core/index.ts
Normal 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";
|
||||
88
src/wallpaper-apis/core/types.ts
Normal file
88
src/wallpaper-apis/core/types.ts
Normal 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; // 允许其他自定义设置
|
||||
};
|
||||
}
|
||||
3
src/wallpaper-apis/index.ts
Normal file
3
src/wallpaper-apis/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
// 导出所有API相关的类和接口
|
||||
export * from "./core";
|
||||
export * from "./providers";
|
||||
2
src/wallpaper-apis/providers/index.ts
Normal file
2
src/wallpaper-apis/providers/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
// 导出所有API提供商实现
|
||||
export * from "./wallhaven";
|
||||
388
src/wallpaper-apis/providers/wallhaven.ts
Normal file
388
src/wallpaper-apis/providers/wallhaven.ts
Normal 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
1400
styles.css
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue