diff --git a/.prettierrc.json b/.prettierrc.json index cdff663..4a6e280 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -2,6 +2,7 @@ "trailingComma": "es5", "tabWidth": 4, "semi": true, + "printWidth": 120, "plugins": [], "overrides": [] -} \ No newline at end of file +} diff --git a/README.md b/README.md index 7ab745c..13a6397 100644 --- a/README.md +++ b/README.md @@ -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)) diff --git a/README.zh.md b/README.zh.md index 8931b6e..e7dc7a9 100644 --- a/README.zh.md +++ b/README.zh.md @@ -10,6 +10,7 @@ - **时间段背景**: 根据时间段自动切换背景 - **手动切换**: 允许用户手动切换背景 +- **🆕 随机壁纸支持**: 从流行的API获取随机壁纸(Unsplash、Pixabay、Pexels),支持间隔模式下的自动切换 - **自定义设置**: 用户可以自定义背景模糊度、亮度、饱和度和颜色 - **支持多种背景类型**: 包括图片、颜色背景和渐变背景,其中图片背景支持远程图片和仓库本地图片 - **可与已有主题兼容**: 该插件可以与现有的 Obsidian 主题一起使用(但会覆盖一些样式,详见[注意](#注意)) diff --git a/esbuild.config.mjs b/esbuild.config.mjs index a5de8b8..b4191ab 100644 --- a/esbuild.config.mjs +++ b/esbuild.config.mjs @@ -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(); } diff --git a/lang/en.json b/lang/en.json deleted file mode 100644 index 6043062..0000000 --- a/lang/en.json +++ /dev/null @@ -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" -} diff --git a/lang/zh-cn.json b/lang/zh-cn.json deleted file mode 100644 index 3c22dc9..0000000 --- a/lang/zh-cn.json +++ /dev/null @@ -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": "拖拽以重新排序" -} diff --git a/main.ts b/main.ts deleted file mode 100644 index aa953e6..0000000 --- a/main.ts +++ /dev/null @@ -1,1690 +0,0 @@ -import { - App, - getLanguage, - ItemView, - Modal, - Notice, - Plugin, - PluginSettingTab, - Setting, - SuggestModal, - WorkspaceLeaf, -} from "obsidian"; - -// 导入语言文件 -import en from "./lang/en.json"; -import zhCN from "./lang/zh-cn.json"; - -const translations: Record = { - en, - zh: zhCN, -}; - -// Global translation function -let translationsForLang = translations["en"] || en; - -function t(key: keyof typeof en, vars?: Record) { - let text = translationsForLang[key] || en[key]; - if (vars) { - for (const varKey in vars) { - text = text.replace(`{${varKey}}`, vars[varKey]); - } - } - return text; -} - -// Initialize translation language -function initializeTranslation() { - // 使用 Obsidian 的 getLanguage() API 获取界面语言 - const lang = getLanguage(); - translationsForLang = translations[lang] || en; -} - -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; // 当前背景索引 -} - -interface TimeRule { - id: string; - name: string; - startTime: string; // "HH:MM" format - endTime: string; // "HH:MM" format - backgroundId: string; - enabled: boolean; -} - -interface BackgroundItem { - id: string; - name: string; - type: "image" | "color" | "gradient"; - value: string; // image URL, color code, or gradient CSS - preview?: string; -} - -let DEFAULT_SETTINGS: DTBSettings; - -function genDefaultSettings() { - 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, - }; -} - -export default class DynamicThemeBackgroundPlugin extends Plugin { - settings: DTBSettings; - intervalId: number | null = null; - styleTag: HTMLStyleElement; - - // 当前的背景 - background: BackgroundItem | null = null; - - async onload() { - // Initialize translation - initializeTranslation(); - // 生成默认设置 - genDefaultSettings(); - - await this.loadSettings(); - - // 打印当前设置 - console.log("Dynamic Theme Background plugin settings:", this.settings); - - // 创建样式元素 - 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)); - - // 添加命令 - this.addCommands(); - - // 启动背景管理器 - if (this.settings.enabled) { - // 等待 layout-ready 事件,确保 vault 完全加载 - this.app.workspace.onLayoutReady(() => { - this.startBackgroundManager(); - }); - } - - console.log("Dynamic Theme Background plugin loaded"); - } - - onunload() { - this.stopBackgroundManager(); - this.deactivateView(); // 清理自定义视图 - this.styleTag?.remove(); - console.log("Dynamic Theme Background plugin unloaded"); - } - - addCommands() { - this.addCommand({ - id: "toggle-dtb", - name: t("command_toggle_name"), - callback: () => { - this.settings.enabled = !this.settings.enabled; - this.saveSettings(); - - if (this.settings.enabled) { - this.startBackgroundManager(); - new Notice(t("command_toggle_enabled_notice")); - } else { - this.stopBackgroundManager(); - new Notice(t("command_toggle_disabled_notice")); - } - }, - }); - - this.addCommand({ - id: "next-background", - name: t("command_next_bg_name"), - callback: () => { - if (this.settings.backgrounds.length > 0) { - this.settings.currentIndex = - (this.settings.currentIndex + 1) % - this.settings.backgrounds.length; - this.background = - this.settings.backgrounds[this.settings.currentIndex]; - this.updateStyleCss(); - this.saveSettings(); - new Notice( - t("command_next_bg_notice", { - bgName: this.background.name, - }) - ); - } - }, - }); - - this.addCommand({ - id: "test-current-background", - name: t("command_test_bg_name"), - callback: () => { - const rule = this.getCurrentTimeRule(); - if (rule) { - const bg = this.settings.backgrounds.find( - (b) => b.id === rule.backgroundId - ); - if (bg) { - this.background = bg; - this.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")); - } - }, - }); - - // 在新标签页中打开设置 - this.addCommand({ - id: "open-dtb-settings-tab", - name: t("command_open_settings_tab_name"), - callback: () => { - this.activateView(); - }, - }); - } - - async loadSettings() { - this.settings = Object.assign( - {}, - DEFAULT_SETTINGS, - 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); - } - - // 将图片路径转换为可用的 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 - } - - // 将十六进制颜色转换为带透明度的rgba格式 - 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})`; - } - - // 更新样式(真正更新背景的地方) - updateStyleCss() { - 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; - } - - // TODO .dtb-enabled 里可能会覆盖已有主题的样式,考虑更好的解法方案 - const bgColorWithOpacity = this.hexToRgba( - this.settings.bgColor, - this.settings.bgColorOpacity - ); - - this.styleTag.innerText = ` - .dtb-enabled { - --background-primary: ${bgColorWithOpacity} !important; - --background-primary-alt: ${bgColorWithOpacity} !important; - --background-secondary: ${bgColorWithOpacity} !important; - --background-secondary-alt: ${bgColorWithOpacity} !important; - --tab-background-active: transparent !important; - --tab-outline-width: transparent !important; - } - .dtb-enabled .workspace::before { - ${backgroundProperty}: ${cssValue} !important; - background-size: cover; - background-repeat: no-repeat; - 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", - }); - } - - 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; - } - - // 更新背景 - updateBackground(forceUpdate: boolean = 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.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; - } - - if (forceUpdate || (needsUpdate && this.background)) { - this.updateStyleCss(); - } - } - - 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(() => { - this.updateBackground(false); - }, intervalMs) - ); - - console.log("DTB: Background manager started", { - mode: this.settings.mode, - interval: intervalMs / 1000 + "s", - }); - } - - stopBackgroundManager() { - if (this.intervalId) { - clearInterval(this.intervalId); - this.intervalId = null; - } - document.body.classList.remove("dtb-enabled"); - console.log("DTB: Background manager stopped"); - } -} - -// 模态窗口 - -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: keyof typeof en; - 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" }); - this.nameInput.addClass("dtb-modal-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(); - inputContainer.addClass("dtb-modal-flex-container-spaced"); - - this.valueInput = inputContainer.createEl("input", { - type: "text", - placeholder, - }); - this.valueInput.addClass("dtb-flex-1"); - - const browseButton = inputContainer.createEl("button", { - text: "Browse", - }); - browseButton.type = "button"; - browseButton.onclick = () => { - const modal = new ImagePathSuggestModal( - this.app, - (imagePath) => { - this.valueInput.value = imagePath; - } - ); - modal.open(); - }; - } else { - this.valueInput = contentEl.createEl("input", { - type: "text", - placeholder, - }); - this.valueInput.addClass("dtb-modal-input-large"); - } - - // Buttons - const buttonContainer = contentEl.createDiv(); - buttonContainer.addClass("dtb-modal-flex-container"); - - const cancelButton = buttonContainer.createEl("button", { - text: t("cancel_button"), - }); - cancelButton.onclick = () => this.close(); - - const submitButton = buttonContainer.createEl("button", { - text: t("confirm_button"), - }); - submitButton.onclick = () => { - this.onSubmit( - this.nameInput.value, - this.valueInput.value || placeholder - ); - this.close(); - }; - } - - onClose() { - const { contentEl } = this; - contentEl.empty(); - } -} - -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, - }); - this.nameInput.addClass("dtb-modal-input"); - - // Start time input - contentEl.createEl("label", { text: t("start_time_label") }); - this.startTimeInput = contentEl.createEl("input", { - type: "time", - value: this.rule.startTime, - }); - this.startTimeInput.addClass("dtb-modal-input"); - - // End time input - contentEl.createEl("label", { text: t("end_time_label") }); - this.endTimeInput = contentEl.createEl("input", { - type: "time", - value: this.rule.endTime, - }); - this.endTimeInput.addClass("dtb-modal-input-large"); - - // Buttons - const buttonContainer = contentEl.createDiv(); - buttonContainer.addClass("dtb-modal-flex-container"); - - const cancelButton = buttonContainer.createEl("button", { - text: t("cancel_button"), - }); - cancelButton.onclick = () => this.close(); - - const submitButton = buttonContainer.createEl("button", { - text: t("confirm_button"), - }); - submitButton.onclick = () => { - this.onSubmit({ - name: this.nameInput.value, - startTime: this.startTimeInput.value, - endTime: this.endTimeInput.value, - }); - this.close(); - }; - } - - onClose() { - const { contentEl } = this; - contentEl.empty(); - } -} - -class ImagePathSuggestModal extends SuggestModal { - 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 []; - } - - // 如果是远程URL,不提供补全 - 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: "image-suggestion-item" }); - - // 创建图标和文本 - const icon = container.createSpan({ cls: "suggestion-icon" }); - icon.textContent = "🖼️"; // 图片图标 - - const text = container.createSpan({ cls: "suggestion-text" }); - text.textContent = imagePath; - - // 添加样式类 - container.addClass("dtb-suggestion-item"); - } - - // 选择建议项时的回调 - onChooseSuggestion(imagePath: string, evt: MouseEvent | KeyboardEvent) { - this.onSubmit(imagePath); - } -} - -class ImageFolderSuggestModal extends SuggestModal { - 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); - } -} - -// 设置面板 -class DTBSettingTab extends PluginSettingTab { - plugin: DynamicThemeBackgroundPlugin; - - constructor(app: App, plugin: DynamicThemeBackgroundPlugin) { - super(app, plugin); - this.plugin = plugin; - } - - display(): void { - const { containerEl } = this; - containerEl.empty(); - - containerEl.createEl("h2", { text: t("settings_title") }); - - // 基础设置 - new Setting(containerEl) - .setName(t("enable_plugin_name")) - .setDesc(t("enable_plugin_desc")) - .addToggle((toggle) => - toggle - .setValue(this.plugin.settings.enabled) - .onChange(async (value) => { - this.plugin.settings.enabled = value; - await this.plugin.saveSettings(); - - if (value) { - this.plugin.startBackgroundManager(); - } else { - this.plugin.stopBackgroundManager(); - } - }) - ) - .addExtraButton((button) => - button - .setIcon("refresh-cw") - .setTooltip(t("reload_plugin_tooltip")) - .onClick(async () => { - // 重新启动背景管理器 - if (this.plugin.settings.enabled) { - this.plugin.startBackgroundManager(); - } - // 强制更新当前背景 - this.plugin.updateBackground(true); - new Notice(t("reload_plugin_notice")); - }) - ); - - // 统一的背景模糊度设置 - new Setting(containerEl) - .setName(t("blur_depth_name")) - .setDesc(t("blur_depth_desc")) - .addSlider((slider) => - slider - .setLimits(0, 30, 1) - .setValue(this.plugin.settings.blurDepth) - .setDynamicTooltip() - .onChange(async (value: number) => { - this.plugin.settings.blurDepth = value; - await this.plugin.saveSettings(); - this.plugin.updateStyleCss(); - }) - ) - .addExtraButton((button) => - button - .setIcon("reset") - .setTooltip(t("reset_blur_tooltip")) - .onClick(async () => { - this.plugin.settings.blurDepth = - DEFAULT_SETTINGS.blurDepth; // 恢复默认值 - await this.plugin.saveSettings(); - this.plugin.updateStyleCss(); - this.display(); - }) - ); - - // 统一的背景亮度设置 - new Setting(containerEl) - .setName(t("brightness_name")) - .setDesc(t("brightness_desc")) - .addSlider((slider) => - slider - .setLimits(0, 1.5, 0.01) - .setValue(this.plugin.settings.brightness4Bg) - .setDynamicTooltip() - .onChange(async (value: number) => { - this.plugin.settings.brightness4Bg = value; - await this.plugin.saveSettings(); - this.plugin.updateStyleCss(); - }) - ) - .addExtraButton((button) => - button - .setIcon("reset") - .setTooltip(t("reset_brightness_tooltip")) - .onClick(async () => { - this.plugin.settings.brightness4Bg = - DEFAULT_SETTINGS.brightness4Bg; // 恢复默认值 - await this.plugin.saveSettings(); - this.plugin.updateStyleCss(); - this.display(); - }) - ); - - // 统一的背景饱和度设置 - new Setting(containerEl) - .setName(t("saturate_name")) - .setDesc(t("saturate_desc")) - .addSlider((slider) => - slider - .setLimits(0, 2, 0.01) - .setValue(this.plugin.settings.saturate4Bg) - .setDynamicTooltip() - .onChange(async (value: number) => { - this.plugin.settings.saturate4Bg = value; - await this.plugin.saveSettings(); - this.plugin.updateStyleCss(); - }) - ) - .addExtraButton((button) => - button - .setIcon("reset") - .setTooltip(t("reset_saturate_tooltip")) - .onClick(async () => { - this.plugin.settings.saturate4Bg = - DEFAULT_SETTINGS.saturate4Bg; // 恢复默认值 - await this.plugin.saveSettings(); - this.plugin.updateStyleCss(); - this.display(); - }) - ); - - // 统一的背景颜色和透明度设置 - new Setting(containerEl) - .setName(t("bg_color_name")) - .setDesc(t("bg_color_desc")) - .addColorPicker((colorPicker) => - colorPicker - .setValue(this.plugin.settings.bgColor) - .onChange(async (value: string) => { - this.plugin.settings.bgColor = value; - await this.plugin.saveSettings(); - this.plugin.updateStyleCss(); - }) - ) - .addSlider((slider) => - slider - .setLimits(0, 1, 0.01) - .setValue(this.plugin.settings.bgColorOpacity) - .setDynamicTooltip() - .onChange(async (value: number) => { - this.plugin.settings.bgColorOpacity = value; - await this.plugin.saveSettings(); - this.plugin.updateStyleCss(); - }) - ) - .addExtraButton((button) => - button - .setIcon("reset") - .setTooltip(t("reset_bg_color_tooltip")) - .onClick(async () => { - this.plugin.settings.bgColor = DEFAULT_SETTINGS.bgColor; - this.plugin.settings.bgColorOpacity = - DEFAULT_SETTINGS.bgColorOpacity; - await this.plugin.saveSettings(); - this.plugin.updateStyleCss(); - this.display(); - }) - ); - - // 背景管理 - containerEl.createEl("h3", { text: t("bg_management_title") }); - - // 添加拖拽提示 - const dragHint = containerEl.createDiv("dtb-drag-hint"); - dragHint.textContent = t("drag_hint_text"); - - const addBgContainer = containerEl.createDiv("dtb-add-bg-container"); - - new Setting(addBgContainer) - .setName(t("add_new_bg_name")) - .addButton((button) => - button - .setButtonText(t("add_image_bg_button")) - .onClick(() => this.showAddBackgroundModal("image")) - ) - .addButton((button) => - button - .setButtonText(t("add_color_bg_button")) - .onClick(() => this.showAddBackgroundModal("color")) - ) - .addButton((button) => - button - .setButtonText(t("add_gradient_bg_button")) - .onClick(() => this.showAddBackgroundModal("gradient")) - ) - .addButton((button) => - button - .setButtonText(t("add_folder_bg_button")) - .onClick(() => this.showAddFolderModal()) - ) - .addExtraButton((button) => - button - .setIcon("reset") - .setTooltip(t("restore_default_bg_tooltip")) - .onClick(() => this.restoreDefaultBackgrounds()) - ); - const backgroundContainer = containerEl.createDiv( - "dtb-background-container" - ); - this.displayBackgrounds(backgroundContainer); - - // 模式设置 - containerEl.createEl("h3", { text: t("mode_settings_title") }); - new Setting(containerEl) - .setName(t("switch_mode_name")) - .setDesc(t("switch_mode_desc")) - .addDropdown((dropdown) => - dropdown - .addOption("time-based", t("mode_time_based")) - .addOption("interval", t("mode_interval")) - .addOption("manual", t("mode_manual")) - .setValue(this.plugin.settings.mode) - .onChange( - async (value: "time-based" | "interval" | "manual") => { - this.plugin.settings.mode = value; - await this.plugin.saveSettings(); - this.plugin.startBackgroundManager(); - this.display(); - } - ) - ); - // 时间规则(仅在时间模式下显示) - if (this.plugin.settings.mode === "time-based") { - containerEl.createEl("h4", { text: t("time_rules_title") }); - // 添加一个恢复默认的按钮 - new Setting(containerEl) - .setName(t("reset_time_rules_name")) - .setDesc(t("reset_time_rules_desc")) - .addButton((button) => - button - .setButtonText(t("reset_button")) - .onClick(async () => { - this.plugin.settings.timeRules = - DEFAULT_SETTINGS.timeRules; - await this.plugin.saveSettings(); - this.display(); - }) - ); - const timeRulesContainer = containerEl.createDiv( - "dtb-time-rules-container" - ); - this.displayTimeRules(timeRulesContainer); - } - // 时间间隔设置(仅在间隔模式下显示) - if (this.plugin.settings.mode === "interval") { - new Setting(containerEl) - .setName(t("interval_name")) - .setDesc(t("interval_desc")) - .addText((text) => - text - .setPlaceholder("60") - .setValue( - this.plugin.settings.intervalMinutes.toString() - ) - .onChange(async (value) => { - const minutes = parseInt(value) || 60; - this.plugin.settings.intervalMinutes = minutes; - await this.plugin.saveSettings(); - }) - ); - } - } - - displayBackgrounds(container: HTMLElement) { - container.empty(); - - this.plugin.settings.backgrounds.forEach((bg, index) => { - const bgEl = container.createDiv("dtb-background-item"); - - // 添加拖拽相关属性 - bgEl.draggable = true; - bgEl.dataset.bgId = bg.id; - bgEl.dataset.index = index.toString(); - - // 添加拖拽手柄 - const dragHandle = bgEl.createDiv("dtb-drag-handle"); - dragHandle.textContent = "⋮⋮"; // 使用双点符号作为拖拽手柄 - dragHandle.title = t("drag_handle_tooltip"); - - const contentDiv = bgEl.createDiv("dtb-bg-content"); - contentDiv.createSpan({ text: bg.name, cls: "dtb-bg-name" }); - contentDiv.createSpan({ text: bg.type, cls: "dtb-bg-type" }); - - // 预览 - const preview = contentDiv.createDiv("dtb-bg-preview"); - this.setPreviewBackground(preview, bg); - - // 操作按钮 - const actions = contentDiv.createDiv("dtb-bg-actions"); - - actions.createEl("button", { text: t("preview_button") }).onclick = - () => { - this.plugin.background = bg; - this.plugin.updateStyleCss(); - }; - - actions.createEl("button", { text: t("delete_button") }).onclick = - async () => { - // 使用 filter 方法删除 - this.plugin.settings.backgrounds = - this.plugin.settings.backgrounds.filter( - (b) => b.id !== bg.id - ); - await this.plugin.saveSettings(); - this.display(); - }; - - // 添加拖拽事件监听器 - this.addDragListeners(bgEl); - }); - } - - // 添加拖拽事件监听器的辅助方法 - addDragListeners(element: HTMLElement) { - element.addEventListener("dragstart", (e) => { - if (e.dataTransfer) { - e.dataTransfer.effectAllowed = "move"; - e.dataTransfer.setData( - "text/plain", - element.dataset.bgId || "" - ); - element.classList.add("dtb-dragging"); - } - }); - - element.addEventListener("dragend", () => { - element.classList.remove("dtb-dragging"); - // 移除所有拖拽相关的样式 - const allItems = element.parentElement?.querySelectorAll( - ".dtb-background-item" - ); - allItems?.forEach((item) => { - item.classList.remove( - "dtb-drag-over", - "dtb-drag-over-top", - "dtb-drag-over-bottom" - ); - }); - }); - - element.addEventListener("dragover", (e) => { - e.preventDefault(); - if (e.dataTransfer) { - e.dataTransfer.dropEffect = "move"; - } - - // 确定拖拽位置(上半部分还是下半部分) - const rect = element.getBoundingClientRect(); - const midpoint = rect.top + rect.height / 2; - const isTopHalf = e.clientY < midpoint; - - // 移除之前的样式 - element.classList.remove( - "dtb-drag-over-top", - "dtb-drag-over-bottom" - ); - - // 添加适当的样式 - if (isTopHalf) { - element.classList.add("dtb-drag-over-top"); - } else { - element.classList.add("dtb-drag-over-bottom"); - } - }); - - element.addEventListener("dragleave", (e) => { - // 只有当鼠标真正离开元素时才移除样式 - if (!element.contains(e.relatedTarget as Node)) { - element.classList.remove( - "dtb-drag-over-top", - "dtb-drag-over-bottom" - ); - } - }); - - element.addEventListener("drop", async (e) => { - e.preventDefault(); - - const draggedId = e.dataTransfer?.getData("text/plain"); - const targetId = element.dataset.bgId; - - if (!draggedId || !targetId || draggedId === targetId) { - return; - } - - // 确定插入位置 - const rect = element.getBoundingClientRect(); - const midpoint = rect.top + rect.height / 2; - const insertAfter = e.clientY >= midpoint; - - await this.reorderBackgrounds(draggedId, targetId, insertAfter); - - // 清理样式 - element.classList.remove( - "dtb-drag-over-top", - "dtb-drag-over-bottom" - ); - }); - } - - // 重新排序背景的方法 - async reorderBackgrounds( - draggedId: string, - targetId: string, - insertAfter: boolean - ) { - const backgrounds = this.plugin.settings.backgrounds; - const draggedIndex = backgrounds.findIndex((bg) => bg.id === draggedId); - const targetIndex = backgrounds.findIndex((bg) => bg.id === targetId); - - if (draggedIndex === -1 || targetIndex === -1) { - console.warn("DTB: Invalid drag operation - background not found"); - return; - } - - // 如果拖拽到相同位置,则不做任何操作 - if ( - draggedIndex === targetIndex || - (insertAfter && draggedIndex === targetIndex + 1) || - (!insertAfter && draggedIndex === targetIndex - 1) - ) { - return; - } - - // 保存被拖拽的背景名称用于用户反馈 - const draggedName = backgrounds[draggedIndex].name; - - // 移除被拖拽的元素 - const draggedItem = backgrounds.splice(draggedIndex, 1)[0]; - - // 计算新的插入位置 - let newTargetIndex = backgrounds.findIndex((bg) => bg.id === targetId); - if (insertAfter) { - newTargetIndex++; - } - - // 插入到新位置 - backgrounds.splice(newTargetIndex, 0, draggedItem); - - // 保存设置并重新显示 - await this.plugin.saveSettings(); - this.display(); - - // 给用户一个成功的反馈 - console.log(`DTB: Successfully reordered background "${draggedName}"`); - } - - displayTimeRules(container: HTMLElement) { - container.empty(); - - this.plugin.settings.timeRules.forEach((rule) => { - const setting = new Setting(container) - .setName(rule.name) - .setDesc(`${rule.startTime} - ${rule.endTime}`) - .addToggle((toggle) => - toggle.setValue(rule.enabled).onChange(async (value) => { - rule.enabled = value; - await this.plugin.saveSettings(); - }) - ) - .addDropdown((dropdown) => { - dropdown.addOption("", t("select_background_option")); - this.plugin.settings.backgrounds.forEach((bg) => { - dropdown.addOption(bg.id, bg.name); - }); - return dropdown - .setValue(rule.backgroundId) - .onChange(async (value) => { - rule.backgroundId = value; - await this.plugin.saveSettings(); - }); - }) - .addButton((button) => - button - .setButtonText(t("edit_button")) - .onClick(() => this.showEditTimeRuleModal(rule)) - ) - .addButton((button) => - button - .setButtonText(t("delete_button")) - .onClick(async () => { - // 使用 filter 方法删除 - this.plugin.settings.timeRules = - this.plugin.settings.timeRules.filter( - (r) => r.id !== rule.id - ); - await this.plugin.saveSettings(); - this.displayTimeRules(container); - }) - ); - }); - } - - async showAddBackgroundModal(type: "image" | "color" | "gradient") { - const modal = new BackgroundModal( - this.app, - type, - async (name, value) => { - if (name && value) { - const newBg: BackgroundItem = { - id: Date.now().toString(), - name, - type, - value, - }; - - this.plugin.settings.backgrounds.push(newBg); - await this.plugin.saveSettings(); - this.display(); - } - } - ); - - modal.open(); - } - - showEditTimeRuleModal(rule: TimeRule) { - const modal = new TimeRuleModal(this.app, rule, async (newRule) => { - rule.name = newRule.name; - rule.startTime = newRule.startTime; - rule.endTime = newRule.endTime; - - await this.plugin.saveSettings(); - this.display(); - }); - - modal.open(); - } - - showAddFolderModal() { - const modal = new ImageFolderSuggestModal( - this.app, - async (folderPath: string) => { - await this.addImagesFromFolder(folderPath); - } - ); - - modal.open(); - } - - async addImagesFromFolder(folderPath: string) { - try { - // 标准化路径:移除开头和结尾的斜杠,只处理 vault 内的相对路径 - folderPath = folderPath.replace(/^\/+|\/+$/g, ""); - - let folderFiles: any[] = []; - - if (folderPath !== "") { - // 尝试获取指定文件夹 - const folder = this.app.vault.getFolderByPath(folderPath); - if (folder) { - // 只获取该文件夹下的直接子文件(不递归) - folderFiles = this.app.vault.getFiles().filter((file) => { - const fileDir = file.path.substring( - 0, - file.path.lastIndexOf("/") - ); - return fileDir === folderPath; - }); - } else { - new Notice(t("folder_not_found")); - return; - } - } - - if (folderFiles.length === 0) { - new Notice(t("folder_not_found")); - return; - } - - await this.processImageFiles(folderFiles, folderPath); - } catch (error) { - console.error("DTB: Error scanning folder:", error); - new Notice(t("folder_scan_error", { error: error.message })); - } - } - - async processImageFiles(files: any[], folderPath: string) { - // 支持的图片格式 - const imageExtensions = [ - ".jpg", - ".jpeg", - ".png", - ".gif", - ".bmp", - ".webp", - ".svg", - ]; - - // 过滤出图片文件 - const imageFiles = files.filter((file) => - imageExtensions.some((ext) => file.path.toLowerCase().endsWith(ext)) - ); - - if (imageFiles.length === 0) { - new Notice(t("folder_not_found")); - return; - } - - let addedCount = 0; - - for (const file of imageFiles) { - // 检查是否已存在相同路径的背景 - const existingBg = this.plugin.settings.backgrounds.find( - (bg) => bg.type === "image" && bg.value === file.path - ); - - if (!existingBg) { - const fileName = file.name.replace(/\.[^/.]+$/, ""); // 移除扩展名 - // 只保留最后一级文件夹名称,避免长路径影响观感 - const folderName = - folderPath === "" - ? "root" - : folderPath.split("/").pop() || folderPath; - const newBg: BackgroundItem = { - id: Date.now().toString() + "-" + addedCount, // 确保ID唯一 - name: `${fileName} (${folderName})`, - type: "image", - value: file.path, - }; - - this.plugin.settings.backgrounds.push(newBg); - addedCount++; - } - } - - if (addedCount > 0) { - await this.plugin.saveSettings(); - this.display(); - new Notice( - t("folder_scan_success", { count: addedCount.toString() }) - ); - } else { - new Notice(t("folder_no_new_images")); - } - } - - /** - * 设置预览元素的背景样式 - * 使用 CSS 自定义属性而不是内联样式,遵循 Obsidian 官方建议 - */ - setPreviewBackground(preview: HTMLElement, bg: BackgroundItem) { - // 移除之前的类型特定类名 - preview.removeClass( - "dtb-preview-image", - "dtb-preview-color", - "dtb-preview-gradient" - ); - - // 清除之前设置的 CSS 自定义属性 - preview.style.removeProperty("--dtb-preview-bg-image"); - preview.style.removeProperty("--dtb-preview-bg"); - - switch (bg.type) { - case "image": - preview.addClass("dtb-preview-image"); - const sanitizedImagePath = this.plugin.sanitizeImagePath( - bg.value - ); - // 只有当图片路径有效时才设置 CSS 变量 - if (sanitizedImagePath && sanitizedImagePath !== "none") { - preview.style.setProperty( - "--dtb-preview-bg-image", - sanitizedImagePath - ); - } - break; - case "color": - case "gradient": - preview.addClass(`dtb-preview-${bg.type}`); - // 验证颜色/渐变值的有效性 - if (bg.value && bg.value.trim()) { - preview.style.setProperty("--dtb-preview-bg", bg.value); - } - break; - default: - console.warn(`DTB: Unknown background type: ${bg.type}`); - break; - } - } - - async restoreDefaultBackgrounds() { - // 重新生成默认设置以获取最新的默认背景 - genDefaultSettings(); - const defaultBackgrounds = DEFAULT_SETTINGS.backgrounds; - - let addedCount = 0; - - // 遍历默认背景,只添加不存在的 - for (const defaultBg of defaultBackgrounds) { - const existingBg = this.plugin.settings.backgrounds.find( - (bg) => bg.id === defaultBg.id - ); - - if (!existingBg) { - // 创建新的背景项,确保 ID 唯一 - const newBg: BackgroundItem = { - id: defaultBg.id, - name: defaultBg.name, - type: defaultBg.type, - value: defaultBg.value, - }; - - this.plugin.settings.backgrounds.push(newBg); - addedCount++; - } - } - - if (addedCount > 0) { - await this.plugin.saveSettings(); - this.display(); - new Notice( - t("restore_default_bg_success", { - count: addedCount.toString(), - }) - ); - } else { - new Notice(t("restore_default_bg_no_new")); - } - } -} - -// 自定义设置视图类 - 用于在标签页中显示设置 -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 { - const container = this.containerEl.children[1]; - container.empty(); - - // 添加标题 - container.createEl("h1", { text: t("settings_title") }); - - // 设置容器样式 - container.addClass("dtb-settings-view"); - - // 使用设置标签页的显示逻辑,但在我们自己的容器中 - if (this.settingTab) { - this.settingTab.containerEl = container as HTMLElement; - this.settingTab.display(); - } - } - - async onClose(): Promise { - // 清理资源 - if (this.settingTab && this.settingTab.containerEl) { - this.settingTab.containerEl.empty(); - this.settingTab.containerEl.removeClass("dtb-settings-view"); - } - this.settingTab = null; // 释放引用,帮助垃圾回收 - this.plugin.deactivateView(); // 确保视图被正确清理 - console.log("DTBSettingsView closed"); - } -} diff --git a/src/commands/fetch-wallpaper-command.ts b/src/commands/fetch-wallpaper-command.ts new file mode 100644 index 0000000..2b42e87 --- /dev/null +++ b/src/commands/fetch-wallpaper-command.ts @@ -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"); + } + }, + }; +} diff --git a/src/commands/index.ts b/src/commands/index.ts new file mode 100644 index 0000000..09e2334 --- /dev/null +++ b/src/commands/index.ts @@ -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); + }); +} diff --git a/src/commands/next-background-command.ts b/src/commands/next-background-command.ts new file mode 100644 index 0000000..3cc5447 --- /dev/null +++ b/src/commands/next-background-command.ts @@ -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, + }) + ); + } + }, + }; +} diff --git a/src/commands/open-settings-command.ts b/src/commands/open-settings-command.ts new file mode 100644 index 0000000..114e867 --- /dev/null +++ b/src/commands/open-settings-command.ts @@ -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(); + }, + }; +} diff --git a/src/commands/test-background-command.ts b/src/commands/test-background-command.ts new file mode 100644 index 0000000..f24b8d8 --- /dev/null +++ b/src/commands/test-background-command.ts @@ -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; + } + } + }, + }; +} diff --git a/src/commands/toggle-command.ts b/src/commands/toggle-command.ts new file mode 100644 index 0000000..4effb04 --- /dev/null +++ b/src/commands/toggle-command.ts @@ -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")); + } + }, + }; +} diff --git a/src/default-settings.ts b/src/default-settings.ts new file mode 100644 index 0000000..6f33468 --- /dev/null +++ b/src/default-settings.ts @@ -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(); +} diff --git a/src/i18n/en.ts b/src/i18n/en.ts new file mode 100644 index 0000000..e8b921b --- /dev/null +++ b/src/i18n/en.ts @@ -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", +}; diff --git a/src/i18n/index.ts b/src/i18n/index.ts new file mode 100644 index 0000000..c9d7106 --- /dev/null +++ b/src/i18n/index.ts @@ -0,0 +1,24 @@ +/** + * 国际化模块 - 处理多语言支持和文本翻译 + * 支持中英文切换,根据Obsidian语言设置自动加载对应翻译 + */ + +import { moment } from "obsidian"; +import en from "./en"; +import zhCN from "./zh-cn"; + +const localeMap: Record = { + en, + "zh-cn": zhCN, +}; + +const locale = localeMap[moment.locale()]; +export function t(str: keyof typeof en, vars?: Record): string { + let text = (locale && locale[str]) || en[str]; + if (vars) { + for (const varKey in vars) { + text = text.replace(`{${varKey}}`, vars[varKey]); + } + } + return text; +} diff --git a/src/i18n/zh-cn.ts b/src/i18n/zh-cn.ts new file mode 100644 index 0000000..315d21b --- /dev/null +++ b/src/i18n/zh-cn.ts @@ -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: "深蓝灰色渐变", +}; diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..3b87858 --- /dev/null +++ b/src/main.ts @@ -0,0 +1 @@ +export { default } from "./plugin"; diff --git a/src/modals/background-modal.ts b/src/modals/background-modal.ts new file mode 100644 index 0000000..fd86219 --- /dev/null +++ b/src/modals/background-modal.ts @@ -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(); + } +} diff --git a/src/modals/image-folder-suggest-modal.ts b/src/modals/image-folder-suggest-modal.ts new file mode 100644 index 0000000..c7b5381 --- /dev/null +++ b/src/modals/image-folder-suggest-modal.ts @@ -0,0 +1,32 @@ +import { App, SuggestModal } from "obsidian"; +import { t } from "../i18n"; + +export class ImageFolderSuggestModal extends SuggestModal { + 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); + } +} diff --git a/src/modals/image-path-suggest-modal.ts b/src/modals/image-path-suggest-modal.ts new file mode 100644 index 0000000..8904e8f --- /dev/null +++ b/src/modals/image-path-suggest-modal.ts @@ -0,0 +1,40 @@ +import { App, SuggestModal } from "obsidian"; + +export class ImagePathSuggestModal extends SuggestModal { + 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); + } +} diff --git a/src/modals/index.ts b/src/modals/index.ts new file mode 100644 index 0000000..49603c2 --- /dev/null +++ b/src/modals/index.ts @@ -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"; diff --git a/src/modals/time-rule-modal.ts b/src/modals/time-rule-modal.ts new file mode 100644 index 0000000..c66dcdb --- /dev/null +++ b/src/modals/time-rule-modal.ts @@ -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(); + } +} diff --git a/src/modals/wallpaper-api-modal.ts b/src/modals/wallpaper-api-modal.ts new file mode 100644 index 0000000..5e16853 --- /dev/null +++ b/src/modals/wallpaper-api-modal.ts @@ -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 = new Map(); + + // 参数配置容器的引用 + paramsSectionContainer: HTMLElement; + + // 额外参数输入 + extraParamsTextarea: HTMLTextAreaElement; + + // Headers配置 + headersContainer: HTMLDivElement; + headerInputs: Array<{ key: HTMLInputElement; value: HTMLInputElement }> = []; + + // 自定义设置输入元素 + customSettingsInputs: Map = 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 = {}; + + 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 = {}; + + // 获取当前参数描述符以便使用转换函数 + 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; + 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 = {}; + this.headerInputs.forEach(({ key, value }) => { + if (key.value.trim() && value.value.trim()) { + headers[key.value.trim()] = value.value.trim(); + } + }); + + // 收集自定义设置 + const customSettings: Record = {}; + 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(); + } +} diff --git a/src/plugin.ts b/src/plugin.ts new file mode 100644 index 0000000..ae24b25 --- /dev/null +++ b/src/plugin.ts @@ -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((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 { + 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 + } +} diff --git a/src/settings/index.ts b/src/settings/index.ts new file mode 100644 index 0000000..1341f2f --- /dev/null +++ b/src/settings/index.ts @@ -0,0 +1,7 @@ +/** + * 设置组件导出 - 统一导出所有设置相关组件 + * 包含设置页面标签页和设置视图等管理界面组件 + */ + +export { DTBSettingTab } from "./settings-tab"; +export { DTBSettingsView, DTB_SETTINGS_VIEW_TYPE } from "./settings-view"; diff --git a/src/settings/settings-tab.ts b/src/settings/settings-tab.ts new file mode 100644 index 0000000..e7ea626 --- /dev/null +++ b/src/settings/settings-tab.ts @@ -0,0 +1,1124 @@ +/** + * 设置标签页,用于显示和管理插件的设置 + * 包括基本设置、背景管理和模式设置等功能 + */ +import { App, Notice, PluginSettingTab, Setting, TFile } from "obsidian"; +import { getDefaultSettings } from "../default-settings"; +import { t } from "../i18n"; +import { BackgroundModal, ImageFolderSuggestModal, TimeRuleModal, WallpaperApiEditorModal } from "../modals"; +import type DynamicThemeBackgroundPlugin from "../plugin"; +import type { BackgroundItem, DTBSettings, TimeRule } from "../types"; +import { + ApiStateSubscriber, + BaseWallpaperApi, + WallpaperApiConfig, + WallpaperApiType, + apiManager, +} from "../wallpaper-apis"; + +export class DTBSettingTab extends PluginSettingTab { + plugin: DynamicThemeBackgroundPlugin; + defaultSettings: DTBSettings; + private componentId: string; + + constructor(app: App, plugin: DynamicThemeBackgroundPlugin) { + super(app, plugin); + this.plugin = plugin; + this.defaultSettings = getDefaultSettings(); + // 生成唯一的组件ID + this.componentId = this.genComponentId(); + } + + display(): void { + // 清理之前的订阅 + this.cleanup(); + + const { containerEl } = this; + containerEl.empty(); + + containerEl.createEl("h2", { text: t("settings_title") }); + + // 创建基本设置容器,便于分组更新 + const basicSettingEl = containerEl.createDiv(); // 这里适合使用默认的 div 容器,因为 displayXXX 内部会清理该容器 + this.displayBasicSettings(basicSettingEl); + + const bgManagementEl = containerEl.createDiv(); // 这里适合使用默认的 div 容器,因为 displayXXX 内部会清理该容器 + this.displayBackgroundManagement(bgManagementEl); + + const wallpaperApiSettingsEl = containerEl.createDiv(); // 这里适合使用默认的 div 容器,因为 displayXXX 内部会清理该容器 + this.displayWallpaperApiSettings(wallpaperApiSettingsEl); + } + + // ============================================================================ + // 基础设置 + // ============================================================================ + + private displayBasicSettings(containerEl: HTMLElement) { + containerEl.empty(); + containerEl.createEl("h3", { text: t("basic_settings_title") }); + + // 基础设置 + new Setting(containerEl) + .setName(t("enable_plugin_name")) + .setDesc(t("enable_plugin_desc")) + .addToggle((toggle) => + toggle.setValue(this.plugin.settings.enabled).onChange(async (value) => { + this.plugin.settings.enabled = value; + await this.plugin.saveSettings(); + if (value) { + this.plugin.startBackgroundManager(); + } else { + this.plugin.stopBackgroundManager(); + } + }) + ) + .addExtraButton((button) => + button + .setIcon("refresh-cw") + .setTooltip(t("reload_plugin_tooltip")) + .onClick(async () => { + // 重新启动背景管理器 + if (this.plugin.settings.enabled) { + this.plugin.startBackgroundManager(); + } + // 强制更新当前背景 + this.plugin.updateBackground(true); + }) + ); + + // 外观设置 + containerEl.createEl("h4", { text: t("appearance_settings_title") }); + // 背景模糊度设置 + new Setting(containerEl) + .setName(t("blur_depth_name")) + .setDesc(t("blur_depth_desc")) + .addSlider((slider) => + slider + .setLimits(0, 30, 1) + .setValue(this.plugin.settings.blurDepth) + .setDynamicTooltip() + .onChange(async (value: number) => { + this.plugin.settings.blurDepth = value; + await this.plugin.saveSettings(); + this.plugin.updateStyleCss(); + }) + ) + .addExtraButton((button) => + button + .setIcon("reset") + .setTooltip(t("reset_blur_tooltip")) + .onClick(async () => { + this.plugin.settings.blurDepth = this.defaultSettings.blurDepth; + await this.plugin.saveSettings(); + this.plugin.updateStyleCss(); + this.displayBasicSettings(containerEl); + }) + ); + + // 背景亮度设置 + new Setting(containerEl) + .setName(t("brightness_name")) + .setDesc(t("brightness_desc")) + .addSlider((slider) => + slider + .setLimits(0, 1.5, 0.01) + .setValue(this.plugin.settings.brightness4Bg) + .setDynamicTooltip() + .onChange(async (value: number) => { + this.plugin.settings.brightness4Bg = value; + await this.plugin.saveSettings(); + this.plugin.updateStyleCss(); + }) + ) + .addExtraButton((button) => + button + .setIcon("reset") + .setTooltip(t("reset_brightness_tooltip")) + .onClick(async () => { + this.plugin.settings.brightness4Bg = this.defaultSettings.brightness4Bg; + await this.plugin.saveSettings(); + this.plugin.updateStyleCss(); + this.displayBasicSettings(containerEl); + }) + ); + + // 背景饱和度设置 + new Setting(containerEl) + .setName(t("saturate_name")) + .setDesc(t("saturate_desc")) + .addSlider((slider) => + slider + .setLimits(0, 2, 0.01) + .setValue(this.plugin.settings.saturate4Bg) + .setDynamicTooltip() + .onChange(async (value: number) => { + this.plugin.settings.saturate4Bg = value; + await this.plugin.saveSettings(); + this.plugin.updateStyleCss(); + }) + ) + .addExtraButton((button) => + button + .setIcon("reset") + .setTooltip(t("reset_saturate_tooltip")) + .onClick(async () => { + this.plugin.settings.saturate4Bg = this.defaultSettings.saturate4Bg; + await this.plugin.saveSettings(); + this.plugin.updateStyleCss(); + this.displayBasicSettings(containerEl); + }) + ); + + // 背景颜色和透明度设置 + new Setting(containerEl) + .setName(t("bg_mask_color_name")) + .setDesc(t("bg_mask_color_desc")) + .addColorPicker((colorPicker) => + colorPicker.setValue(this.plugin.settings.bgColor).onChange(async (value: string) => { + this.plugin.settings.bgColor = value; + await this.plugin.saveSettings(); + this.plugin.updateStyleCss(); + }) + ) + .addSlider((slider) => + slider + .setLimits(0, 1, 0.01) + .setValue(this.plugin.settings.bgColorOpacity) + .setDynamicTooltip() + .onChange(async (value: number) => { + this.plugin.settings.bgColorOpacity = value; + await this.plugin.saveSettings(); + this.plugin.updateStyleCss(); + }) + ) + .addExtraButton((button) => + button + .setIcon("reset") + .setTooltip(t("reset_bg_mask_color_tooltip")) + .onClick(async () => { + this.plugin.settings.bgColor = this.defaultSettings.bgColor; + this.plugin.settings.bgColorOpacity = this.defaultSettings.bgColorOpacity; + await this.plugin.saveSettings(); + this.plugin.updateStyleCss(); + this.displayBasicSettings(containerEl); + }) + ); + + // 模式设置 + containerEl.createEl("h4", { text: t("mode_settings_title") }); + new Setting(containerEl) + .setName(t("switch_mode_name")) + .setDesc(t("switch_mode_desc")) + .addDropdown((dropdown) => + dropdown + .addOption("time-based", t("mode_time_based")) + .addOption("interval", t("mode_interval")) + .addOption("manual", t("mode_manual")) + .setValue(this.plugin.settings.mode) + .onChange(async (value: "time-based" | "interval" | "manual") => { + this.plugin.settings.mode = value; + await this.plugin.saveSettings(); + this.plugin.startBackgroundManager(); + this.displayBasicSettings(containerEl); + }) + ); + + // 时间规则(仅在时间模式下显示) + if (this.plugin.settings.mode === "time-based") { + containerEl.createEl("h4", { text: t("time_rules_title") }); + new Setting(containerEl) + .setName(t("manage_time_rules_name")) + .setDesc(t("manage_time_rules_desc")) + .addButton((button) => { + button.setButtonText(t("add_time_rule_button")); + button.setTooltip(t("add_time_rule_tooltip")); + button.buttonEl.addClass("dtb-action-button"); + button.onClick(() => this.showTimeRuleModal()); + }) + .addButton((button) => { + button.setButtonText(t("clear_time_rules_button")); + button.setTooltip(t("clear_time_rules_tooltip")); + button.buttonEl.addClass("dtb-action-button"); + button.onClick(async () => { + this.plugin.settings.timeRules = []; + await this.plugin.saveSettings(); + this.displayBasicSettings(containerEl); + }); + }) + .addButton((button) => { + button.setButtonText(t("reset_time_rules_button")); + button.setTooltip(t("reset_time_rules_tooltip")); + button.buttonEl.addClass("dtb-action-button"); + button.onClick(async () => { + this.plugin.settings.timeRules = this.defaultSettings.timeRules; + await this.plugin.saveSettings(); + this.displayBasicSettings(containerEl); + }); + }); + + // 添加时间规则提示 + const hint = containerEl.createDiv("dtb-hint"); + hint.textContent = t("time_rule_hint"); + + // 显示时间规则列表 + const timeRulesContainer = containerEl.createDiv("dtb-item-list-container dtb-section-container"); + this.displayTimeRules(timeRulesContainer); + } + + // 时间间隔设置(仅在间隔模式下显示) + if (this.plugin.settings.mode === "interval") { + new Setting(containerEl) + .setName(t("interval_name")) + .setDesc(t("interval_desc")) + .addText((text) => + text + .setPlaceholder("60") + .setValue(this.plugin.settings.intervalMinutes.toString()) + .onChange(async (value) => { + const minutes = parseInt(value) || 60; + this.plugin.settings.intervalMinutes = minutes; + await this.plugin.saveSettings(); + this.plugin.startBackgroundManager(); + }) + ); + + // 随机壁纸设置 + new Setting(containerEl) + .setName(t("enable_random_wallpaper_name")) + .setDesc(t("enable_random_wallpaper_desc")) + .addToggle((toggle) => + toggle.setValue(this.plugin.settings.enableRandomWallpaper).onChange(async (value) => { + this.plugin.settings.enableRandomWallpaper = value; + await this.plugin.saveSettings(); + this.displayBasicSettings(containerEl); + }) + ); + } + } + + // 显示时间规则列表,支持编辑、删除和添加新规则 + private displayTimeRules(container: HTMLElement): void { + container.empty(); + + this.plugin.settings.timeRules.forEach((rule: TimeRule) => { + new Setting(container) + .setName(rule.name) + .setDesc(`${rule.startTime} - ${rule.endTime}`) + .addToggle((toggle) => + toggle.setValue(rule.enabled).onChange(async (value) => { + rule.enabled = value; + await this.plugin.saveSettings(); + }) + ) + .addDropdown((dropdown) => { + dropdown.addOption("", t("select_background_option")); + this.plugin.settings.backgrounds.forEach((bg) => { + dropdown.addOption(bg.id, bg.name); + }); + return dropdown.setValue(rule.backgroundId).onChange(async (value) => { + rule.backgroundId = value; + await this.plugin.saveSettings(); + this.plugin.updateBackground(); + }); + }) + .addButton((button) => + button.setButtonText(t("button_edit")).onClick(() => this.showTimeRuleModal(rule)) + ) + .addButton((button) => + button.setButtonText(t("button_delete")).onClick(async () => { + // 使用 filter 方法删除 + this.plugin.settings.timeRules = this.plugin.settings.timeRules.filter((r) => r.id !== rule.id); + await this.plugin.saveSettings(); + this.displayTimeRules(container); + }) + ); + }); + } + + // 显示添加或编辑时间规则的模态窗口 + private showTimeRuleModal(rule?: TimeRule) { + // 如果没有提供规则,创建一个新的空规则 + const editRule: TimeRule = rule || { + id: "", + name: "", + startTime: "09:00", + endTime: "17:00", + backgroundId: "", + enabled: true, + }; + + const modal = new TimeRuleModal(this.app, editRule, async (updatedRule) => { + if (!updatedRule.name.trim() || !updatedRule.startTime || !updatedRule.endTime) { + new Notice("Please provide all required fields"); + return; + } + + if (rule) { + // 编辑现有规则 + const index = this.plugin.settings.timeRules.findIndex((r) => r.name === rule.name); + if (index !== -1) { + this.plugin.settings.timeRules[index] = { + ...rule, + name: updatedRule.name.trim(), + startTime: updatedRule.startTime, + endTime: updatedRule.endTime, + }; + } + } else { + // 添加新规则 + const newRule: TimeRule = { + id: Date.now().toString() + Math.random().toString(36).substring(2, 9), + name: updatedRule.name.trim(), + startTime: updatedRule.startTime, + endTime: updatedRule.endTime, + backgroundId: this.plugin.settings.backgrounds[0]?.id || "", + enabled: true, + }; + this.plugin.settings.timeRules.push(newRule); + } + + await this.plugin.saveSettings(); + this.display(); + }); + + modal.open(); + } + + // ============================================================================ + // 背景管理 + // ============================================================================ + + private displayBackgroundManagement(containerEl: HTMLElement) { + containerEl.empty(); + + // 背景管理 + containerEl.createEl("h3", { text: t("bg_management_title") }); + + const addBgContainer = containerEl.createDiv("dtb-add-bg-container"); + + // 添加拖拽提示 + const dragHint = containerEl.createDiv("dtb-hint"); + dragHint.textContent = t("drag_hint_text"); + + new Setting(addBgContainer) + .setName(t("add_new_bg_name")) + .setDesc(t("add_new_bg_desc")) + .addButton((button) => + button.setButtonText(t("add_image_bg_button")).onClick(() => this.showAddBackgroundModal("image")) + ) + .addButton((button) => + button.setButtonText(t("add_color_bg_button")).onClick(() => this.showAddBackgroundModal("color")) + ) + .addButton((button) => + button.setButtonText(t("add_gradient_bg_button")).onClick(() => this.showAddBackgroundModal("gradient")) + ) + .addButton((button) => + button.setButtonText(t("add_folder_bg_button")).onClick(() => this.showAddFolderModal()) + ) + .addExtraButton((button) => + button + .setIcon("reset") + .setTooltip(t("restore_default_bg_tooltip")) + .onClick(() => this.restoreDefaultBackgrounds()) + ); + + const backgroundContainer = containerEl.createDiv("dtb-item-list-container dtb-section-container"); + this.displayBackgrounds(backgroundContainer); + } + + // 显示添加或编辑背景的模态窗口 + private async showAddBackgroundModal(type: "image" | "color" | "gradient") { + const modal = new BackgroundModal(this.app, type, async (name: string, value: string) => { + if (!name.trim() || !value.trim()) { + new Notice("Please provide both name and value"); + return; + } + + // 生成唯一ID + const id = Date.now().toString() + Math.random().toString(36).substring(2, 9); + + // 创建新的背景项 + const newBackground: BackgroundItem = { + id, + name: name.trim(), + type, + value: value.trim(), + }; + + // 添加到设置中 + this.plugin.settings.backgrounds.push(newBackground); + await this.plugin.saveSettings(); + + // 直接全刷新 + this.display(); + }); + + modal.open(); + } + + // 显示编辑背景的模态窗口 + private async showEditBackgroundModal(bg: BackgroundItem, index: number) { + const modal = new BackgroundModal(this.app, bg.type, async (name: string, value: string) => { + if (!name.trim() || !value.trim()) { + new Notice("Please provide both name and value"); + return; + } + + // 更新现有背景项 + this.plugin.settings.backgrounds[index] = { + ...bg, + name: name.trim(), + value: value.trim(), + }; + + await this.plugin.saveSettings(); + + // 如果当前正在使用这个背景,则更新显示 + if (this.plugin.background?.id === bg.id) { + this.plugin.background = this.plugin.settings.backgrounds[index]; + this.plugin.updateStyleCss(); + } + + // 刷新显示 + this.display(); + }); + + // 先打开模态窗口,然后预填充现有值 + modal.open(); + + // 等待模态窗口完全加载后再设置值 + setTimeout(() => { + if (modal.nameInput && modal.valueInput) { + modal.nameInput.value = bg.name; + modal.valueInput.value = bg.value; + } + }, 0); + } + + private showAddFolderModal() { + const modal = new ImageFolderSuggestModal(this.app, async (folderPath: string) => { + if (!folderPath.trim()) { + new Notice("Please provide a valid folder path"); + return; + } + + // 检查文件夹是否存在 + const folder = this.app.vault.getAbstractFileByPath(folderPath); + if (!folder) { + new Notice("Folder not found"); + return; + } + + try { + // 处理文件夹中的图片文件 + if (typeof this.addImagesFromFolder === "function") { + await this.addImagesFromFolder(folderPath); + new Notice(`Images from folder "${folderPath}" added successfully`); + } else { + new Notice("Folder processing method not available"); + } + } catch (error) { + console.error("Error adding images from folder:", error); + new Notice("Error adding images from folder"); + } + }); + + modal.open(); + } + + private async restoreDefaultBackgrounds() { + // 重新生成默认设置以获取最新的默认背景 + const defaultBackgrounds = this.defaultSettings.backgrounds; + + let addedCount = 0; + + // 遍历默认背景,只添加不存在的 + for (const defaultBg of defaultBackgrounds) { + const existingBg = this.plugin.settings.backgrounds.find((bg) => bg.id === defaultBg.id); + + if (!existingBg) { + // 创建新的背景项,确保 ID 唯一 + const newBg: BackgroundItem = { + id: defaultBg.id, + name: defaultBg.name, + type: defaultBg.type, + value: defaultBg.value, + }; + + this.plugin.settings.backgrounds.push(newBg); + addedCount++; + } + } + + if (addedCount > 0) { + await this.plugin.saveSettings(); + this.display(); + new Notice( + t("restore_default_bg_success", { + count: addedCount.toString(), + }) + ); + } else { + new Notice(t("restore_default_bg_no_new")); + } + } + + // 在指定的容器元素中渲染所有背景项 + private displayBackgrounds(container: HTMLElement): void { + container.empty(); + + this.plugin.settings.backgrounds.forEach((bg: BackgroundItem, index: number) => { + const bgEl = container.createDiv("dtb-item dtb-draggable"); + + // 添加拖拽相关属性 + bgEl.draggable = true; + bgEl.dataset.bgId = bg.id; + bgEl.dataset.index = index.toString(); + + // 添加拖拽手柄 + const dragHandle = bgEl.createDiv("dtb-drag-handle"); + dragHandle.textContent = "⋮⋮"; // 使用双点符号作为拖拽手柄 + dragHandle.title = t("drag_handle_tooltip"); + + const contentDiv = bgEl.createDiv("dtb-bg-content"); + contentDiv.createSpan({ text: bg.name, cls: "dtb-bg-name" }); + contentDiv.createSpan({ text: bg.type, cls: "dtb-type-badge" }); + + // 预览 + const preview = contentDiv.createDiv("dtb-bg-preview"); + this.setPreviewBackground(preview, bg); + + // 操作按钮 + const actions = contentDiv.createDiv("dtb-actions"); + + actions.createEl("button", { text: t("button_preview") }).onclick = () => { + this.plugin.background = bg; + this.plugin.settings.currentIndex = index; // 更新当前索引 + this.plugin.saveSettings(); + this.plugin.updateStyleCss(); + }; + + actions.createEl("button", { text: t("button_edit") }).onclick = () => { + this.showEditBackgroundModal(bg, index); + }; + + actions.createEl("button", { text: t("button_delete") }).onclick = async () => { + // 使用 filter 方法删除 + this.plugin.settings.backgrounds = this.plugin.settings.backgrounds.filter( + (b: BackgroundItem) => b.id !== bg.id + ); + await this.plugin.saveSettings(); + this.displayBackgrounds(container); + }; + + // 添加拖拽事件监听器 + this.addDragListeners(bgEl); + }); + } + + // 添加拖拽事件监听器的辅助方法 + private addDragListeners(element: HTMLElement) { + element.addEventListener("dragstart", (e) => { + if (e.dataTransfer) { + e.dataTransfer.effectAllowed = "move"; + e.dataTransfer.setData("text/plain", element.dataset.bgId || ""); + element.classList.add("dtb-dragging"); + } + }); + + element.addEventListener("dragend", () => { + element.classList.remove("dtb-dragging"); + // 移除所有拖拽相关的样式 + const allItems = element.parentElement?.querySelectorAll(".dtb-item"); + allItems?.forEach((item) => { + item.classList.remove("dtb-drag-over", "dtb-drag-over-top", "dtb-drag-over-bottom"); + }); + }); + + element.addEventListener("dragover", (e) => { + e.preventDefault(); + if (e.dataTransfer) { + e.dataTransfer.dropEffect = "move"; + } + + // 确定拖拽位置(上半部分还是下半部分) + const rect = element.getBoundingClientRect(); + const midpoint = rect.top + rect.height / 2; + const isTopHalf = e.clientY < midpoint; + + // 移除之前的样式 + element.classList.remove("dtb-drag-over-top", "dtb-drag-over-bottom"); + + // 添加适当的样式 + if (isTopHalf) { + element.classList.add("dtb-drag-over-top"); + } else { + element.classList.add("dtb-drag-over-bottom"); + } + }); + + element.addEventListener("dragleave", (e) => { + // 只有当鼠标真正离开元素时才移除样式 + if (!element.contains(e.relatedTarget as Node)) { + element.classList.remove("dtb-drag-over-top", "dtb-drag-over-bottom"); + } + }); + + element.addEventListener("drop", async (e) => { + e.preventDefault(); + + const draggedId = e.dataTransfer?.getData("text/plain"); + const targetId = element.dataset.bgId; + + if (!draggedId || !targetId || draggedId === targetId) { + return; + } + + // 确定插入位置 + const rect = element.getBoundingClientRect(); + const midpoint = rect.top + rect.height / 2; + const insertAfter = e.clientY >= midpoint; + + await this.reorderBackgrounds(draggedId, targetId, insertAfter); + + // 清理样式 + element.classList.remove("dtb-drag-over-top", "dtb-drag-over-bottom"); + }); + } + + // 重新排序背景的方法 + private async reorderBackgrounds(draggedId: string, targetId: string, insertAfter: boolean) { + const backgrounds = this.plugin.settings.backgrounds; + const draggedIndex = backgrounds.findIndex((bg: BackgroundItem) => bg.id === draggedId); + const targetIndex = backgrounds.findIndex((bg: BackgroundItem) => bg.id === targetId); + + if (draggedIndex === -1 || targetIndex === -1) { + console.warn("DTB: Invalid drag operation - background not found"); + return; + } + + // 如果拖拽到相同位置,则不做任何操作 + if ( + draggedIndex === targetIndex || + (insertAfter && draggedIndex === targetIndex + 1) || + (!insertAfter && draggedIndex === targetIndex - 1) + ) { + return; + } + + // 保存被拖拽的背景名称用于用户反馈 + const draggedName = backgrounds[draggedIndex].name; + + // 移除被拖拽的元素 + const draggedItem = backgrounds.splice(draggedIndex, 1)[0]; + + // 计算新的插入位置 + let newTargetIndex = backgrounds.findIndex((bg: BackgroundItem) => bg.id === targetId); + if (insertAfter) { + newTargetIndex++; + } + + // 插入到新位置 + backgrounds.splice(newTargetIndex, 0, draggedItem); + + // 保存设置并重新显示 + await this.plugin.saveSettings(); + this.display(); + + // 给用户一个成功的反馈 + console.debug(`DTB: Successfully reordered background "${draggedName}"`); + } + + // 设置预览元素的背景样式 * 使用 CSS 自定义属性而不是内联样式,遵循 Obsidian 官方建议 + private setPreviewBackground(preview: HTMLElement, bg: BackgroundItem): void { + // 移除之前的类型特定类名 + preview.removeClass("dtb-preview-image", "dtb-preview-color", "dtb-preview-gradient"); + + // 清除之前设置的 CSS 自定义属性 + preview.style.removeProperty("--dtb-preview-bg-image"); + preview.style.removeProperty("--dtb-preview-bg"); + + switch (bg.type) { + case "image": { + preview.addClass("dtb-preview-image"); + const sanitizedImagePath = this.plugin.sanitizeImagePath(bg.value); + // 只有当图片路径有效时才设置 CSS 变量 + if (sanitizedImagePath && sanitizedImagePath !== "none") { + preview.style.setProperty("--dtb-preview-bg-image", sanitizedImagePath); + } + break; + } + case "color": + case "gradient": { + preview.addClass(`dtb-preview-${bg.type}`); + // 验证颜色/渐变值的有效性 + if (bg.value && bg.value.trim()) { + preview.style.setProperty("--dtb-preview-bg", bg.value); + } + break; + } + default: + console.warn(`DTB: Unknown background type: ${bg.type}`); + break; + } + } + + // 添加文件夹中的图片到背景列表 + private async addImagesFromFolder(folderPath: string) { + try { + // 标准化路径:移除开头和结尾的斜杠,只处理 vault 内的相对路径 + folderPath = folderPath.replace(/^\/+|\/+$/g, ""); + + let folderFiles: TFile[] = []; + + if (folderPath !== "") { + // 尝试获取指定文件夹 + const folder = this.app.vault.getFolderByPath(folderPath); + if (folder) { + // 只获取该文件夹下的直接子文件(不递归) + folderFiles = this.app.vault.getFiles().filter((file) => { + const fileDir = file.path.substring(0, file.path.lastIndexOf("/")); + return fileDir === folderPath; + }); + } else { + new Notice(t("folder_not_found")); + return; + } + } + + if (folderFiles.length === 0) { + new Notice(t("folder_not_found")); + return; + } + + await this.processImageFiles(folderFiles, folderPath); + } catch (error) { + console.error("DTB: Error scanning folder:", error); + new Notice(t("folder_scan_error", { error: error.message })); + } + } + + // 处理文件夹中的图片文件 + private async processImageFiles(files: TFile[], folderPath: string) { + // 支持的图片格式 + const imageExtensions = [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".svg"]; + + // 过滤出图片文件 + const imageFiles = files.filter((file) => imageExtensions.some((ext) => file.path.toLowerCase().endsWith(ext))); + + if (imageFiles.length === 0) { + new Notice(t("folder_not_found")); + return; + } + + let addedCount = 0; + + for (const file of imageFiles) { + // 检查是否已存在相同路径的背景 + const existingBg = this.plugin.settings.backgrounds.find( + (bg) => bg.type === "image" && bg.value === file.path + ); + + if (!existingBg) { + const fileName = file.name.replace(/\.[^/.]+$/, ""); // 移除扩展名 + // 只保留最后一级文件夹名称,避免长路径影响观感 + const folderName = folderPath === "" ? "root" : folderPath.split("/").pop() || folderPath; + const newBg: BackgroundItem = { + id: Date.now().toString() + "-" + addedCount, // 确保ID唯一 + name: `${fileName} (${folderName})`, + type: "image", + value: file.path, + }; + + this.plugin.settings.backgrounds.push(newBg); + addedCount++; + } + } + + if (addedCount > 0) { + await this.plugin.saveSettings(); + this.display(); + new Notice(t("folder_scan_success", { count: addedCount.toString() })); + } else { + new Notice(t("folder_no_new_images")); + } + } + + // ============================================================================ + // 壁纸 API 管理 + // ============================================================================ + + /* + * 显示壁纸 API 管理设置 + */ + private displayWallpaperApiSettings(containerEl: HTMLElement): void { + containerEl.empty(); + + containerEl.createEl("h3", { text: t("wallpaper_api_management_title") }); + + // 添加 API 按钮 + new Setting(containerEl) + .setName(t("add_api_name")) + .setDesc(t("add_api_desc")) + .addButton((button) => { + button.setButtonText(t("add_api_button")); + button.buttonEl.addClass("dtb-action-button"); + button.onClick(() => this.showAddWallpaperApiModal()); + }); + + // 添加 API 提示 + const hint = containerEl.createDiv("dtb-hint"); + hint.textContent = t("wallpaper_api_hint"); + + // 显示现有API列表 + const apiContainer = containerEl.createDiv("dtb-item-list-container"); + this.displayWallpaperApis(apiContainer); + } + + /* + * 显示所有已配置的壁纸 API + */ + private displayWallpaperApis(container: HTMLElement) { + container.empty(); + + // API 列表 + this.plugin.settings.wallpaperApis.forEach((apiConfig: WallpaperApiConfig, index: number) => { + const apiInstance = apiManager.getApiById(apiConfig.id); + if (!apiInstance) { + console.warn(`DTB: API instance not found for ${apiConfig.name}`); + return; + } + + // 描述 + const desc = apiInstance.getDescription(); + + const setting = new Setting(container).setName(apiConfig.name).setDesc(desc); + + // 在设置项的控件区域直接添加类型标签 + setting.controlEl.createSpan({ text: apiConfig.type || "Unknown", cls: "dtb-type-badge" }); + + // 注意:状态指示器和启用按钮的状态都以 API 实例的状态为准,配置项的 enabled 字段仅用于初始状态和保存设置时的同步。 + + // 添加状态指示器 + const statusIndicator = setting.controlEl.createDiv("dtb-api-status"); + const statusDot = statusIndicator.createDiv("dtb-api-status-dot"); + const statusText = statusIndicator.createSpan(); + // 根据API的启用状态设置初始状态 + if (apiInstance.getEnabled()) { + statusDot.addClass("enabled"); + statusText.textContent = "Enabled"; + } else { + statusDot.addClass("disabled"); + statusText.textContent = "Disabled"; + } + + // 创建 toggle 并保存引用 + let toggleComponent: { setValue: (value: boolean) => void; getValue: () => boolean } | null = null; + setting.addToggle((toggle) => { + toggleComponent = toggle; // 保存 toggle 引用 + const toggleEl = toggle.setValue(apiInstance.getEnabled()); + + // 使用智能API管理方法 + toggleEl.onChange(async (value) => { + // 禁用toggle防止用户重复点击,并添加loading样式 + toggle.setDisabled(true); + toggleEl.toggleEl.addClass("dtb-loading"); + + try { + let success: boolean; + + if (value) { + // 使用智能启用方法 + success = await apiManager.enableApi(apiConfig.id); + } else { + // 使用智能禁用方法 + success = await apiManager.disableApi(apiConfig.id); + } + + if (!success) { + new Notice(`❌ Failed to ${value ? "enable" : "disable"} ${apiConfig.name}`, 3000); + } else { + new Notice(`✅ Successfully ${value ? "enabled" : "disabled"} ${apiConfig.name}`, 3000); + } + } catch (error) { + console.error(`DTB: Error ${value ? "enabling" : "disabling"} API:`, error); + new Notice(`❌ Error ${value ? "enabling" : "disabling"} ${apiConfig.name}`, 3000); + } finally { + // 重新启用toggle并移除loading样式 + toggle.setDisabled(false); + toggleEl.toggleEl.removeClass("dtb-loading"); + } + }); + + return toggleEl; + }); + + // 订阅状态变化,使用 ApiStateSubscriber 对象进行标识 + const subscriber = new ApiStateSubscriber("toggle", this.componentId, apiConfig.id); + apiManager.stateManager.subscribe(subscriber, async (state) => { + // 更新状态点的样式 + statusDot.removeClass("enabled", "disabled", "error", "loading"); + + if (state.isLoading) { + statusDot.addClass("loading"); + statusText.textContent = "Loading..."; + } else if (state.error) { + statusDot.addClass("error"); + statusText.textContent = "Error"; + statusText.title = state.error; + // 同步更新 toggle 状态 + if (toggleComponent && toggleComponent.getValue() !== false) { + toggleComponent.setValue(false); + } + apiConfig.enabled = false; + await this.plugin.saveSettings(); + } else if (state.instanceEnabled) { + statusDot.addClass("enabled"); + statusText.textContent = "Enabled"; + // 同步更新 toggle 状态 + if (toggleComponent && toggleComponent.getValue() !== true) { + toggleComponent.setValue(true); + } + apiConfig.enabled = true; + await this.plugin.saveSettings(); + } else { + statusDot.addClass("disabled"); + statusText.textContent = "Disabled"; + // 同步更新 toggle 状态 + if (toggleComponent && toggleComponent.getValue() !== false) { + toggleComponent.setValue(false); + } + apiConfig.enabled = false; + await this.plugin.saveSettings(); + } + }); + + setting + .addButton((button) => + button + .setButtonText(t("button_add")) + .setTooltip(t("add_api_bg_tooltip")) + .onClick(async () => { + await this.fetchWallpaperFromApi(apiInstance); + }) + ) + .addButton((button) => + button.setButtonText(t("button_edit")).onClick(() => { + this.showEditWallpaperApiModal(apiConfig, index); + }) + ) + .addButton((button) => + button.setButtonText(t("button_delete")).onClick(async () => { + // 删除API实例 + apiManager.deleteApi(apiConfig.id); + // 删除插件设置中的API配置 + this.plugin.settings.wallpaperApis = this.plugin.settings.wallpaperApis.filter( + (api) => api.id !== apiConfig.id + ); + await this.plugin.saveSettings(); + this.displayWallpaperApis(container); + }) + ); + }); + } + + // 显示添加壁纸API的模态窗口 + private showAddWallpaperApiModal() { + const emptyConfig: WallpaperApiConfig = { + id: "", + name: "", + type: WallpaperApiType.Custom, + baseUrl: "", + enabled: false, + params: {}, + }; + + const modal = new WallpaperApiEditorModal(this.app, emptyConfig, async (apiConfig) => { + // 创建新的API实例 + apiManager.createApi(apiConfig); + // 添加到插件设置中 + this.plugin.settings.wallpaperApis.push(apiConfig); + await this.plugin.saveSettings(); + this.display(); + }); + + modal.open(); + } + + // 显示编辑壁纸API的模态窗口 + private showEditWallpaperApiModal(apiConfig: WallpaperApiConfig, index: number) { + const modal = new WallpaperApiEditorModal(this.app, apiConfig, async (updatedConfig) => { + // 有可能api类型也修改了,干脆重新创建API实例覆盖原来的 + apiManager.createApi(updatedConfig); + + this.plugin.settings.wallpaperApis[index] = updatedConfig; + await this.plugin.saveSettings(); + this.display(); + }); + + modal.open(); + } + + // 从API获取壁纸并添加到背景列表 + private async fetchWallpaperFromApi(api: BaseWallpaperApi) { + if (!api.getEnabled()) { + new Notice(`❌ ${api.getName()}: API is disabled`); + return; + } + + try { + // 显示加载提示 + const loadingNotice = new Notice(`🔄 Fetching wallpaper from ${api.getName()}...`, 0); + + // 使用API管理器获取随机壁纸 + const wallpaperImages = await apiManager.getRandomWallpapers(api.getId()); + + // 关闭加载提示 + loadingNotice.hide(); + + if (wallpaperImages) { + // 创建新的图片背景项 + const newBg: BackgroundItem = { + id: `${api.getId()}-${Date.now()}`, + name: `${api.getName()} - ${new Date().toLocaleString()}`, + type: "image", + value: wallpaperImages[0].url, + }; + + // 添加到背景列表 + this.plugin.settings.backgrounds.push(newBg); + await this.plugin.saveSettings(); + + // 立即应用这个背景 + this.plugin.background = newBg; + this.plugin.updateStyleCss(); + + // 刷新显示 + this.display(); + + new Notice(`✅ Successfully applied wallpaper from ${api.getName()}`); + } else { + new Notice(`❌ Failed to fetch wallpaper from ${api.getName()}`); + } + } catch (error) { + new Notice(`❌ Error fetching wallpaper from ${api.getName()}: ${error.message}`); + console.error("DTB: Error fetching wallpaper:", error); + } + } + + /** + * 清理所有订阅 + */ + cleanup(): void { + // 使用组件ID清理该组件的所有订阅 + apiManager.stateManager.cleanupByComponent(this.componentId); + } + + // ============================================================================ + // Utils + // ============================================================================ + + private genComponentId() { + return `settings-tab-${crypto.randomUUID()}`; + } +} diff --git a/src/settings/settings-view.ts b/src/settings/settings-view.ts new file mode 100644 index 0000000..f2f7f00 --- /dev/null +++ b/src/settings/settings-view.ts @@ -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 { + 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 { + // 清理设置标签页的订阅 + if (this.settingTab) { + this.settingTab.cleanup(); + } + + // 清理资源 + if (this.settingTab && this.settingTab.containerEl) { + this.settingTab.containerEl.empty(); + } + this.settingTab = null; // 释放引用,帮助垃圾回收 + this.plugin.deactivateView(); // 确保视图被正确清理 + } +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..3ce74c9 --- /dev/null +++ b/src/types.ts @@ -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; +} diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..f81ee97 --- /dev/null +++ b/src/utils.ts @@ -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})`; +} diff --git a/src/wallpaper-apis/core/api-error.ts b/src/wallpaper-apis/core/api-error.ts new file mode 100644 index 0000000..20f2965 --- /dev/null +++ b/src/wallpaper-apis/core/api-error.ts @@ -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 + ) { + 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}`; + } + } +} diff --git a/src/wallpaper-apis/core/api-manager.ts b/src/wallpaper-apis/core/api-manager.ts new file mode 100644 index 0000000..49cd13d --- /dev/null +++ b/src/wallpaper-apis/core/api-manager.ts @@ -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(); + + 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 { + // 验证参数 + 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 成功返回true,失败返回false + */ + async enableApi(apiId: string): Promise { + // 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 成功返回true,失败返回false + */ + async disableApi(apiId: string): Promise { + // 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 { + 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(); diff --git a/src/wallpaper-apis/core/api-registry.ts b/src/wallpaper-apis/core/api-registry.ts new file mode 100644 index 0000000..2565176 --- /dev/null +++ b/src/wallpaper-apis/core/api-registry.ts @@ -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(); + + 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(); diff --git a/src/wallpaper-apis/core/api-state-manager.ts b/src/wallpaper-apis/core/api-state-manager.ts new file mode 100644 index 0000000..96199bc --- /dev/null +++ b/src/wallpaper-apis/core/api-state-manager.ts @@ -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 } + > = new Map(); + + /** + * 订阅API状态变化 + * @param subscriber 订阅者对象 + * @param callback 状态变化回调(支持同步和异步) + */ + subscribe(subscriber: ApiStateSubscriber, callback: (state: ApiState) => void | Promise): () => 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[] = []; + + 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(); + const components = new Set(); + + 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(); + 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; +} diff --git a/src/wallpaper-apis/core/base-api.ts b/src/wallpaper-apis/core/base-api.ts new file mode 100644 index 0000000..209456e --- /dev/null +++ b/src/wallpaper-apis/core/base-api.ts @@ -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; // 启用插件时必须调用 + abstract deinit(): Promise; // 禁用插件时必须调用 + abstract updateImageCache(): Promise; // 更新图片缓存数据 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 { + 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 { + 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 { + 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 { + 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; + } +} diff --git a/src/wallpaper-apis/core/index.ts b/src/wallpaper-apis/core/index.ts new file mode 100644 index 0000000..4e7cf00 --- /dev/null +++ b/src/wallpaper-apis/core/index.ts @@ -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"; diff --git a/src/wallpaper-apis/core/types.ts b/src/wallpaper-apis/core/types.ts new file mode 100644 index 0000000..052f709 --- /dev/null +++ b/src/wallpaper-apis/core/types.ts @@ -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; // 请求头 + params: WallpaperApiParams; + + // 自定义设置 + customSettings?: { + imageUrlJsonPath?: string; // 从响应中提取图片URL的JSON路径 + [key: string]: string | undefined; // 允许其他自定义设置 + }; +} diff --git a/src/wallpaper-apis/index.ts b/src/wallpaper-apis/index.ts new file mode 100644 index 0000000..6422c30 --- /dev/null +++ b/src/wallpaper-apis/index.ts @@ -0,0 +1,3 @@ +// 导出所有API相关的类和接口 +export * from "./core"; +export * from "./providers"; diff --git a/src/wallpaper-apis/providers/index.ts b/src/wallpaper-apis/providers/index.ts new file mode 100644 index 0000000..1d7098c --- /dev/null +++ b/src/wallpaper-apis/providers/index.ts @@ -0,0 +1,2 @@ +// 导出所有API提供商实现 +export * from "./wallhaven"; diff --git a/src/wallpaper-apis/providers/wallhaven.ts b/src/wallpaper-apis/providers/wallhaven.ts new file mode 100644 index 0000000..f88600a --- /dev/null +++ b/src/wallpaper-apis/providers/wallhaven.ts @@ -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 { + 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 { + if (!this.initialized) { + return true; + } + + // 清理缓存数据 + this.wallpaperImageCache = []; + this.curDataIndex = 0; + this.currentPage = 1; + + this.initialized = false; + return true; + } + + async updateImageCache(): Promise { + // 如果已经到最后一页了,跳到第一页 + if (this.totalPages > 0 && this.currentPage > this.totalPages) { + this.currentPage = 1; + } + const success = await this.fetchAndCachePageImages(this.currentPage); + if (success) { + this.currentPage += 1; + } + return success; + } + + // ============================================================================ + // 辅助方法 + // ============================================================================ + + // 拉取指定页码的壁纸数据并缓存到 wallpaperImageCache + private async fetchAndCachePageImages(page = this.currentPage): Promise { + 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) => 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): 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); diff --git a/styles.css b/styles.css index e26804d..737f60b 100644 --- a/styles.css +++ b/styles.css @@ -1,31 +1,47 @@ -/* =========================== - Obsidian 动态主题背景插件样式 - Dynamic Theme Background Plugin Styles - =========================== */ - /* ============================================================================= - 1. 基础容器和背景项样式 (Base Container & Background Item Styles) + Obsidian 动态主题背景插件样式 + Dynamic Theme Background Plugin Styles + + 包含了插件中广泛使用的通用样式组件。所有样式都使用 dtb- 前缀以避免命名冲突。 + + 主要组件分类: + ┌───────────────────────────────────────────────────────────────────────── + │ 🧩 布局组件 (Layout Components) + │ • .dtb-item - 通用条目容器,带悬停效果和动画 + │ • .dtb-item-list-container - 设置列表容器,用于包装setting-item + │ • .dtb-actions - 操作按钮组容器 + │ • .dtb-*-container - 各种布局容器(flex, list, section等) + ├───────────────────────────────────────────────────────────────────────── + │ 📝 表单控件 (Form Controls) + │ • .dtb-input - 统一的输入框样式 + │ • .dtb-dropdown - 下拉选择框,带自定义箭头 + │ • .dtb-checkbox-container - 复选框容器,统一间距和布局 + │ • .dtb-field-container - 表单字段包装器,含标签和描述 + ├───────────────────────────────────────────────────────────────────────── + │ 🎨 视觉元素 (Visual Elements) + │ • .dtb-type-badge - 类型标签徽章,渐变背景 + │ • .dtb-hint - 提示信息框,带左侧强调条 + │ • .dtb-bg-preview - 背景预览缩略图,支持多种类型 + ├───────────────────────────────────────────────────────────────────────── + │ 🔘 交互组件 (Interactive Components) + │ • .dtb-action-button - 操作按钮,统一的悬停和点击效果 + │ • .dtb-remove-button - 删除按钮,圆形红色警告样式 + │ • .dtb-suggestion-item - 建议选项,用于自动完成 + └───────────────────────────────────────────────────────────────────────── + + 使用指南: + • 所有组件都支持 Obsidian 主题的 CSS 变量,确保良好的主题兼容性 + • 响应式设计:在 600px 以下自动调整为移动端布局 + • 统一的动画和过渡效果,使用 cubic-bezier 缓动函数 + • 无障碍友好:正确的焦点状态和键盘导航支持 + + 注意事项: + ⚠️ 请勿直接修改这些通用样式,如需定制请创建特定的覆盖样式 + ⚠️ 新增组件时请遵循现有的命名规范和设计模式 ============================================================================= */ -/* 背景容器 - 主要的外层容器 */ -.dtb-background-container { - margin: 1rem 0; - background: var(--background-primary-alt); - border: 1px solid var(--background-modifier-border); - border-radius: 12px; - padding: 1rem; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); - transition: all 0.2s ease; -} - -/* 背景容器悬停效果 */ -.dtb-background-container:hover { - box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08); - border-color: var(--interactive-accent); -} - -/* 背景项目 - 单个背景项的容器 */ -.dtb-background-item { +/* 通用条目容器 */ +.dtb-item { display: flex; align-items: center; gap: 1.2rem; @@ -37,11 +53,10 @@ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); position: relative; overflow: hidden; - cursor: grab; /* 支持拖拽 */ + /* 继承通用拖拽样式 */ } - /* 背景项左侧装饰条 */ -.dtb-background-item::before { +.dtb-item::before { content: ""; position: absolute; left: 0; @@ -52,53 +67,192 @@ opacity: 0; transition: opacity 0.2s ease; } - /* 背景项悬停效果 */ -.dtb-background-item:hover { +.dtb-item:hover { border-color: var(--background-modifier-border-hover); background: var(--background-secondary); transform: translateY(-1px); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); } - /* 背景项悬停时显示装饰条 */ -.dtb-background-item:hover::before { +.dtb-item:hover::before { opacity: 1; } - /* 最后一个背景项移除底部边距 */ -.dtb-background-item:last-child { +.dtb-item:last-child { margin-bottom: 0; } +/* 上滑入场动画 */ +@keyframes slideInUp { + from { + opacity: 0; + transform: translateY(20px); + } -/* 背景项内容容器 */ -.dtb-bg-content { + to { + opacity: 1; + transform: translateY(0); + } +} +/* 背景项目的分阶段入场动画 */ +.dtb-item { + animation: slideInUp 0.2s ease-out; + animation-fill-mode: both; +} +.dtb-item:nth-child(1) { + animation-delay: 0.05s; +} +.dtb-item:nth-child(2) { + animation-delay: 0.1s; +} +.dtb-item:nth-child(3) { + animation-delay: 0.15s; +} +.dtb-item:nth-child(4) { + animation-delay: 0.2s; +} +.dtb-item:nth-child(5) { + animation-delay: 0.25s; +} + +/* 通用列表容器样式 */ + +/* 设置列表项样式 */ +.dtb-item-list-container .setting-item { + border: 1px solid var(--background-modifier-border); + border-radius: 12px; + padding: 0.8rem; + margin-bottom: 0.4rem; + background: var(--background-primary-alt); + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; + overflow: hidden; + display: flex; + flex-direction: row; + align-items: flex-start; + gap: 1rem; +} +/* 设置项信息区域 - 左侧动态宽度区域 */ +.dtb-item-list-container .setting-item .setting-item-info { + flex: 1; /* 占用剩余空间 */ + min-width: 0; /* 允许收缩 */ + word-wrap: break-word; + overflow-wrap: break-word; + hyphens: auto; + margin-right: 1rem; /* 与右侧保持间距 */ +} +/* 设置项名称 */ +.dtb-item-list-container .setting-item .setting-item-name { + font-weight: 600; + color: var(--text-normal); + font-size: 1rem; + margin-bottom: 0.3rem; + line-height: 1.3; +} +/* 设置项描述文本优化 - 在动态区域内换行 */ +.dtb-item-list-container .setting-item .setting-item-description { + word-wrap: break-word; + overflow-wrap: break-word; + word-break: break-word; + hyphens: auto; + line-height: 1.4; + color: var(--text-muted); + font-size: 0.9rem; + white-space: normal; /* 允许正常换行 */ +} +/* 设置项控件区域 - 右侧固定内容宽度 */ +.dtb-item-list-container .setting-item .setting-item-control { + flex: 0 0 auto; /* 不扩展,不收缩,内容宽度 */ display: flex; align-items: center; - gap: 1.2rem; - flex: 1; + justify-content: flex-end; + gap: 0.5rem; + flex-wrap: nowrap; /* 坚决不换行 */ + white-space: nowrap; /* 防止内容换行 */ +} +/* 设置列表项顶部装饰条 */ +.dtb-item-list-container .setting-item::before { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3px; + background: linear-gradient(90deg, var(--interactive-accent), var(--interactive-accent-hover)); + opacity: 0; + transition: opacity 0.2s ease; +} +/* 设置列表项悬停效果 */ +.dtb-item-list-container .setting-item:hover { + border-color: var(--interactive-accent); + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08); + transform: translateY(-2px); +} +/* 悬停时显示顶部装饰条 */ +.dtb-item-list-container .setting-item:hover::before { + opacity: 1; /* 完全显示 */ } -/* ============================================================================= - 2. 背景项元素样式 (Background Item Elements) - ============================================================================= */ - -/* 背景名称 */ -.dtb-bg-name { - font-weight: 600; - flex: 1; - font-size: 0.95rem; - letter-spacing: -0.01em; +/* 通用操作按钮容器,用于创建一组按钮 */ +.dtb-actions { + display: flex; + gap: 0.6rem; +} +/* 操作按钮 */ +.dtb-actions button, +.dtb-action-button, +.dtb-item-list-container button { + /* 让 dtb-item-list-container 中的按钮也应用按钮样式 */ + padding: 0.5rem 1rem; + border-radius: 8px; + border: 1px solid var(--background-modifier-border); + background: var(--background-primary); color: var(--text-normal); + cursor: pointer; + font-size: 0.8rem; + font-weight: 500; + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; + overflow: hidden; +} +/* 按钮 */ +.dtb-actions button::before, +.dtb-action-button::before, +.dtb-item-list-container button::before { + content: ""; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent); + transition: left 0.5s ease; +} +/* 按钮悬停效果 */ +.dtb-actions button:hover, +.dtb-action-button:hover, +.dtb-item-list-container button:hover { + background: var(--interactive-hover); + border-color: var(--interactive-accent); + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); +} +.dtb-actions button:hover::before, +.dtb-action-button:hover::before, +.dtb-item-list-container button:hover::before { + left: 100%; +} +/* 按钮点击效果 */ +.dtb-actions button:active, +.dtb-action-button:active, +.dtb-item-list-container button:active { + transform: translateY(0); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } -/* 背景类型标签 */ -.dtb-bg-type { - background: linear-gradient( - 135deg, - var(--interactive-accent), - var(--interactive-accent-hover) - ); +/* 通用类型标签徽章*/ +.dtb-type-badge { + background: linear-gradient(135deg, var(--interactive-accent), var(--interactive-accent-hover)); color: var(--text-on-accent); padding: 0.4rem 0.8rem; border-radius: 20px; @@ -108,6 +262,327 @@ letter-spacing: 0.5px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); border: none; + display: inline-block; + cursor: default; + user-select: none; +} + +/* 提示信息 */ +.dtb-hint { + font-size: 0.9em; + color: var(--text-muted); + margin-bottom: 10px; + padding: 12px 16px; + background: linear-gradient(135deg, var(--background-secondary), var(--background-primary-alt)); + border-radius: 8px; + border-left: 4px solid var(--interactive-accent); + display: flex; + align-items: center; + gap: 8px; + transition: all 0.2s ease; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); +} +/* 拖拽提示悬停效果 */ +.dtb-hint:hover { + transform: translateX(4px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); +} + +/* 通用输入框样式 */ +.dtb-input { + width: 100%; + padding: 8px 12px; + margin-bottom: 10px; + min-height: 32px; + line-height: 1.4; + border: 1px solid var(--background-modifier-border); + border-radius: 6px; + background-color: var(--background-primary); + color: var(--text-normal); + font-size: var(--font-ui-medium); + transition: border-color 0.2s ease; +} +.dtb-input:focus { + outline: none; + border-color: var(--interactive-accent); + box-shadow: 0 0 0 2px var(--interactive-accent-hover); +} + +/* 通用下拉选择框样式 */ +.dtb-dropdown { + width: 100%; + padding: 10px 12px; + margin-bottom: 10px; + min-height: 36px; + height: 36px; + line-height: 16px; + border: 1px solid var(--background-modifier-border); + border-radius: 6px; + background-color: var(--background-primary); + color: var(--text-normal); + font-size: var(--font-ui-medium); + cursor: pointer; + box-sizing: border-box; + transition: border-color 0.2s ease; + appearance: none; + -webkit-appearance: none; + -moz-appearance: none; + /* 使用更好的下拉箭头图标 */ + background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23888888' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6,9 12,15 18,9'%3e%3c/polyline%3e%3c/svg%3e"); + background-repeat: no-repeat; + background-position: right 12px center; + background-size: 16px; + padding-right: 40px; + /* 确保文字垂直居中 */ + vertical-align: middle; +} +.dtb-dropdown:focus { + outline: none; + border-color: var(--interactive-accent); + box-shadow: 0 0 0 2px var(--interactive-accent-hover); +} +.dtb-dropdown option { + background-color: var(--background-primary); + color: var(--text-normal); + padding: 4px 8px; +} + +/* 通用分区容器样式 */ +.dtb-section-container { + margin-bottom: 24px; + padding: 16px; + border: 1px solid var(--background-modifier-border); + border-radius: 8px; + background-color: var(--background-secondary); +} + +/* 通用字段容器样式 */ +.dtb-field-container { + margin-bottom: 16px; +} +.dtb-field-container label { + display: block; + margin-bottom: 4px; + font-weight: 500; + color: var(--text-normal); +} +.dtb-field-description { + font-style: italic; + color: var(--text-muted); + font-weight: normal; +} + +/* 通用列表容器样式 */ +.dtb-list-container { + margin-bottom: 12px; +} +.dtb-list-row { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 8px; +} +.dtb-list-row .dtb-input { + flex: 1; + margin-bottom: 0; +} + +/* 通用删除按钮样式 */ +.dtb-remove-button { + background-color: var(--background-modifier-error); + color: var(--text-on-accent); + border: none; + border-radius: 50%; + width: 24px; + height: 24px; + cursor: pointer; + font-weight: bold; + display: flex; + align-items: center; + justify-content: center; + transition: background-color 0.2s ease; +} +.dtb-remove-button:hover { + background-color: var(--background-modifier-error-hover); +} + +/* 通用多选容器样式 */ +.dtb-multiselect-container { + border: 1px solid var(--background-modifier-border); + border-radius: 4px; + padding: 8px; + background-color: var(--background-primary); +} + +/* 通用复选框容器样式 */ +.dtb-checkbox-container { + display: flex; + align-items: center; + margin-bottom: 4px; +} +.dtb-checkbox-container:last-child { + margin-bottom: 0; +} +.dtb-checkbox-container label { + margin: 0 0 0 8px; + font-weight: normal; +} + +/* 通用按钮容器样式 */ +.dtb-flex-container-end { + display: flex; + justify-content: flex-end; + gap: 8px; + margin-top: 15px; +} +.dtb-flex-container-center { + display: flex; + justify-content: center; + gap: 8px; + margin-top: 15px; +} +.dtb-flex-container-spaced { + display: flex; + justify-content: space-between; + gap: 12px; + margin-top: 20px; +} + +/* 建议项样式 */ +.dtb-suggestion-item { + padding: 8px 12px; + cursor: pointer; + border-radius: 4px; + transition: background-color 0.15s ease; + display: flex; + align-items: center; + gap: 8px; +} +.dtb-suggestion-item:hover { + background: var(--background-modifier-hover); +} +.dtb-suggestion-item.is-selected { + background: var(--interactive-accent); + color: var(--text-on-accent); +} + +/* 弹性布局工具类 */ +.dtb-flex-1 { + flex: 1; +} + +/* ============================================================================= + 拖拽功能样式 (Drag and Drop Functionality) - 通用化设计 + + 使用方法: + 1. 为可拖拽元素添加 .dtb-draggable 类 + 2. 拖拽时添加 .dtb-dragging 类 + 3. 放置区域使用 .dtb-drag-over-top 或 .dtb-drag-over-bottom 类 + 4. 拖拽手柄使用 .dtb-drag-handle 类 + + 示例: +
+
⋮⋮
+ +
+ ============================================================================= */ + +/* 可拖拽元素的基础样式 */ +.dtb-draggable { + cursor: grab; + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); +} +.dtb-draggable:hover { + transform: translateY(-1px); +} +/* 拖拽状态样式(通用) */ +.dtb-dragging { + opacity: 0.5; + cursor: grabbing; + transform: rotate(2deg) scale(0.98); + z-index: 1000; +} +/* 拖拽手柄 */ +.dtb-drag-handle { + color: var(--text-muted); + cursor: grab; + padding: 4px; + user-select: none; + font-weight: bold; + line-height: 1; + transition: all 0.2s ease; + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; + min-width: 24px; + height: 24px; +} +.dtb-drag-handle:hover { + color: var(--text-normal); + background: var(--background-modifier-hover); + transform: scale(1.1); +} +/* 拖拽放置区域样式 - 顶部(通用) */ +.dtb-drag-over-top { + border-top: 3px solid var(--interactive-accent); + border-top-left-radius: 10px; + border-top-right-radius: 10px; + margin-top: 6px; + position: relative; +} +/* 拖拽放置区域顶部指示线(通用) */ +.dtb-drag-over-top::before { + content: ""; + position: absolute; + top: -8px; + left: 0; + right: 0; + height: 2px; + background: var(--interactive-accent); + border-radius: 1px; + box-shadow: 0 0 8px var(--interactive-accent); +} +/* 拖拽放置区域样式 - 底部(通用) */ +.dtb-drag-over-bottom { + border-bottom: 3px solid var(--interactive-accent); + border-bottom-left-radius: 10px; + border-bottom-right-radius: 10px; + margin-bottom: 6px; + position: relative; +} +/* 拖拽放置区域底部指示线(通用) */ +.dtb-drag-over-bottom::after { + content: ""; + position: absolute; + bottom: -8px; + left: 0; + right: 0; + height: 2px; + background: var(--interactive-accent); + border-radius: 1px; + box-shadow: 0 0 8px var(--interactive-accent); +} + +/* ============================================================================= + 背景项元素样式 (Background Item Elements) + ============================================================================= */ + +/* 背景项内容容器 */ +.dtb-bg-content { + display: flex; + align-items: center; + gap: 1.2rem; + flex: 1; +} + +/* 背景名称 */ +.dtb-bg-name { + font-weight: 600; + flex: 1; + font-size: 0.95rem; + letter-spacing: -0.01em; + color: var(--text-normal); } /* 背景预览图 */ @@ -126,7 +601,6 @@ background-position: center; background-repeat: no-repeat; } - /* 图片类型预览的特殊样式 */ .dtb-bg-preview.dtb-preview-image { background-image: var(--dtb-preview-bg-image, none); @@ -134,7 +608,6 @@ background-position: center; background-repeat: no-repeat; } - /* 颜色和渐变类型预览的特殊样式 */ .dtb-bg-preview.dtb-preview-color, .dtb-bg-preview.dtb-preview-gradient { @@ -143,7 +616,6 @@ background-position: initial; background-repeat: initial; } - /* 预览图加载失败时的占位样式 */ .dtb-bg-preview::before { content: ""; @@ -154,14 +626,10 @@ transition: opacity 0.2s ease; z-index: -1; } - /* 当背景图片加载失败时显示占位符 */ -.dtb-bg-preview.dtb-preview-image:not( - [style*="--dtb-preview-bg-image"] - )::before { +.dtb-bg-preview.dtb-preview-image:not([style*="--dtb-preview-bg-image"])::before { opacity: 1; } - /* 预览图内侧光晕效果 */ .dtb-bg-preview::after { content: ""; @@ -170,647 +638,373 @@ border-radius: 6px; box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.1); } - /* 预览图悬停效果 */ .dtb-bg-preview:hover { transform: scale(1.05); box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15); } -/* 操作按钮容器 */ -.dtb-bg-actions { - display: flex; - gap: 0.6rem; -} - -/* 操作按钮 */ -.dtb-bg-actions button { - padding: 0.5rem 1rem; - border-radius: 8px; - border: 1px solid var(--background-modifier-border); - background: var(--background-primary); - color: var(--text-normal); - cursor: pointer; - font-size: 0.8rem; - font-weight: 500; - transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); - position: relative; - overflow: hidden; -} - -/* 按钮悬停光效 */ -.dtb-bg-actions button::before { - content: ""; - position: absolute; - top: 0; - left: -100%; - width: 100%; - height: 100%; - background: linear-gradient( - 90deg, - transparent, - rgba(255, 255, 255, 0.1), - transparent - ); - transition: left 0.5s ease; -} - -.dtb-bg-actions button:hover { - background: var(--interactive-hover); - border-color: var(--interactive-accent); - transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); -} - -.dtb-bg-actions button:hover::before { - left: 100%; -} - -.dtb-bg-actions button:active { - transform: translateY(0); - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); -} - -/* ============================================================================= - 3. 拖拽功能样式 (Drag and Drop Functionality) - ============================================================================= */ - -/* 拖拽状态样式 */ -.dtb-background-item.dtb-dragging { - opacity: 0.5; - cursor: grabbing; - transform: rotate(2deg) scale(0.98); - z-index: 1000; -} - -/* 拖拽手柄 */ -.dtb-drag-handle { - color: var(--text-muted); - cursor: grab; - padding: 4px; - user-select: none; - font-weight: bold; - line-height: 1; - transition: all 0.2s ease; - border-radius: 4px; - display: flex; - align-items: center; - justify-content: center; - min-width: 24px; - height: 24px; -} - -.dtb-drag-handle:hover { - color: var(--text-normal); - background: var(--background-modifier-hover); - transform: scale(1.1); -} - -/* 拖拽放置区域样式 - 顶部 */ -.dtb-background-item.dtb-drag-over-top { - border-top: 3px solid var(--interactive-accent); - border-top-left-radius: 10px; - border-top-right-radius: 10px; - margin-top: 6px; - position: relative; -} - -/* 拖拽放置区域顶部指示线 */ -.dtb-background-item.dtb-drag-over-top::before { - content: ""; - position: absolute; - top: -8px; - left: 0; - right: 0; - height: 2px; - background: var(--interactive-accent); - border-radius: 1px; - box-shadow: 0 0 8px var(--interactive-accent); -} - -/* 拖拽放置区域样式 - 底部 */ -.dtb-background-item.dtb-drag-over-bottom { - border-bottom: 3px solid var(--interactive-accent); - border-bottom-left-radius: 10px; - border-bottom-right-radius: 10px; - margin-bottom: 6px; - position: relative; -} - -/* 拖拽放置区域底部指示线 */ -.dtb-background-item.dtb-drag-over-bottom::after { - content: ""; - position: absolute; - bottom: -8px; - left: 0; - right: 0; - height: 2px; - background: var(--interactive-accent); - border-radius: 1px; - box-shadow: 0 0 8px var(--interactive-accent); -} - -/* 拖拽提示信息 */ -.dtb-drag-hint { - font-size: 0.9em; - color: var(--text-muted); - margin-bottom: 10px; - padding: 12px 16px; - background: linear-gradient( - 135deg, - var(--background-secondary), - var(--background-primary-alt) - ); - border-radius: 8px; - border-left: 4px solid var(--interactive-accent); - display: flex; - align-items: center; - gap: 8px; - transition: all 0.2s ease; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); -} - -/* 拖拽提示悬停效果 */ -.dtb-drag-hint:hover { - transform: translateX(4px); - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); -} - -/* ============================================================================= - 4. 设置视图样式 (Settings View Styles) - ============================================================================= */ - -/* 设置视图主容器 */ -.dtb-settings-view { - padding: 2rem; - max-width: 900px; - margin: 0 auto; - background: var(--background-primary); - border-radius: 16px; - box-shadow: 0 4px 24px rgba(0, 0, 0, 0.06); - animation: slideInUp 0.3s ease-out; -} - -/* 主标题 */ -.dtb-settings-view h1 { - border-bottom: 3px solid var(--interactive-accent); - padding-bottom: 1rem; - margin-bottom: 2rem; - color: var(--text-accent); - font-size: 1.8rem; - font-weight: 700; - letter-spacing: -0.02em; - background: linear-gradient( - 135deg, - var(--text-accent), - var(--interactive-accent) - ); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} - -/* 二级标题 */ -.dtb-settings-view h3 { - margin-top: 2rem; - margin-bottom: 1rem; - color: var(--text-accent); - font-size: 1.3rem; - font-weight: 600; - position: relative; - padding-left: 1rem; -} - -/* 二级标题左侧装饰条 */ -.dtb-settings-view h3::before { - content: ""; - position: absolute; - left: 0; - top: 50%; - transform: translateY(-50%); - width: 4px; - height: 1.2rem; - background: linear-gradient( - 135deg, - var(--interactive-accent), - var(--interactive-accent-hover) - ); - border-radius: 2px; -} - -/* 三级标题 */ -.dtb-settings-view h4 { - margin-top: 1.2rem; - margin-bottom: 0.8rem; - color: var(--text-muted); - font-size: 1.1rem; - font-weight: 500; -} - -/* 时间规则容器样式 */ -.dtb-settings-view .dtb-time-rules-container .setting-item { - border: 1px solid var(--background-modifier-border); - border-radius: 12px; - padding: 0.8rem; - margin-bottom: 0.4rem; - background: var(--background-primary-alt); - transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); - position: relative; - overflow: hidden; -} - -/* 时间规则项顶部装饰条 */ -.dtb-settings-view .dtb-time-rules-container .setting-item::before { - content: ""; - position: absolute; - top: 0; - left: 0; - right: 0; - height: 3px; - background: linear-gradient( - 90deg, - var(--interactive-accent), - var(--interactive-accent-hover) - ); - opacity: 0; - transition: opacity 0.2s ease; -} - -/* 时间规则项悬停效果 */ -.dtb-settings-view .dtb-time-rules-container .setting-item:hover { - border-color: var(--interactive-accent); - box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08); - transform: translateY(-2px); -} - -/* 悬停时显示顶部装饰条 */ -.dtb-settings-view .dtb-time-rules-container .setting-item:hover::before { - opacity: 1; /* 完全显示 */ -} - -/* 时间规则项名称样式 */ -.dtb-settings-view .dtb-time-rules-container .setting-item-name { - font-weight: 600; - color: var(--text-normal); - font-size: 1rem; -} - /* 添加背景容器样式 */ -.dtb-settings-view .dtb-add-bg-container .setting-item-control { +.dtb-add-bg-container .setting-item-control { display: flex; gap: 1rem; flex-wrap: wrap; align-items: center; } - /* 添加背景按钮样式 */ -.dtb-settings-view .dtb-add-bg-container button { +.dtb-add-bg-container button { border-radius: 10px; padding: 0.8rem 1.5rem; font-weight: 600; transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); border: 2px solid transparent; - background: linear-gradient( - 135deg, - var(--interactive-accent), - var(--interactive-accent-hover) - ); + background: linear-gradient(135deg, var(--interactive-accent), var(--interactive-accent-hover)); color: var(--text-on-accent); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } - /* 添加背景按钮悬停效果 */ -.dtb-settings-view .dtb-add-bg-container button:hover { +.dtb-add-bg-container button:hover { transform: translateY(-3px); box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15); border-color: var(--background-modifier-border); } -/* 通用设置项样式增强 */ -.dtb-settings-view .setting-item { - border-radius: 10px; - margin-bottom: 0.3rem; - transition: all 0.2s ease; -} - -/* 设置项悬停效果 */ -.dtb-settings-view .setting-item:hover { - background: var(--background-secondary); -} - -/* 时间规则基础样式 */ -.setting-item.mod-toggle .setting-item-control { - display: flex; - align-items: center; - gap: 0.8rem; - padding: 0.2rem 0; -} - /* ============================================================================= - 5. 模态窗口样式 (Modal Dialog Styles) + API管理样式 (API Management Styles) ============================================================================= */ -/* 输入框样式 */ -.dtb-modal-input { - width: 100%; - margin-bottom: 10px; +/* API loading状态样式 */ +.dtb-loading { + position: relative; + opacity: 0.6; + pointer-events: none; } -.dtb-modal-input-large { - width: 100%; - margin-bottom: 15px; +.dtb-loading::after { + content: ""; + position: absolute; + top: 50%; + left: 50%; + width: 12px; + height: 12px; + margin: -6px 0 0 -6px; + border: 2px solid transparent; + border-top: 2px solid var(--interactive-accent); + border-radius: 50%; + animation: dtb-spin 1s linear infinite; } -/* 文本区域样式 */ -.dtb-modal-textarea { - width: 100%; - min-height: 80px; - margin-bottom: 10px; - resize: vertical; +@keyframes dtb-spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } } -/* 选择框样式 */ -.dtb-modal-dropdown { - width: 100%; - margin-bottom: 10px; -} - -/* 标签样式 */ -.dtb-modal-label { - display: block; - margin-bottom: 8px; - font-weight: 500; - color: var(--text-normal); -} - -/* 复选框容器样式 */ -.dtb-modal-checkbox-container { - display: flex; +/* API状态指示器 */ +.dtb-api-status { + display: inline-flex; align-items: center; - gap: 8px; - margin-bottom: 15px; + gap: 4px; + font-size: 0.8em; + margin-left: 8px; } -.dtb-modal-checkbox-container label { - margin: 0; - cursor: pointer; - user-select: none; +.dtb-api-status-dot { + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--text-muted); } -/* 容器样式 */ -.dtb-modal-flex-container { - display: flex; - justify-content: flex-end; - gap: 8px; - margin-top: 15px; +.dtb-api-status-dot.enabled { + background: var(--color-green); } -.dtb-modal-flex-container-center { - display: flex; - justify-content: center; - gap: 8px; - margin-top: 15px; +.dtb-api-status-dot.disabled { + background: var(--text-muted); } -.dtb-modal-flex-container-spaced { - display: flex; - justify-content: space-between; - gap: 12px; - margin-top: 20px; +.dtb-api-status-dot.error { + background: var(--color-red); } -/* 按钮组样式 */ -.dtb-modal-button-group { - display: flex; - gap: 10px; - margin-top: 10px; -} - -/* 提示文本样式 */ -.dtb-modal-hint-text { - font-size: 0.9em; - color: var(--text-muted); - margin-bottom: 15px; - padding: 10px; - background: var(--background-secondary); - border-radius: 6px; - border-left: 3px solid var(--interactive-accent); -} - -.dtb-modal-description { - font-size: 0.85em; - color: var(--text-muted); - margin-bottom: 10px; - line-height: 1.4; -} - -/* 分隔符样式 */ -.dtb-modal-separator { - margin: 20px 0; - height: 1px; - background: var(--background-modifier-border); -} - -/* ============================================================================= - 6. 建议和自动完成样式 (Suggestion & Autocomplete Styles) - ============================================================================= */ - -/* 建议项样式 */ -.dtb-suggestion-item { - padding: 8px 12px; - cursor: pointer; - border-radius: 4px; - transition: background-color 0.15s ease; - display: flex; - align-items: center; - gap: 8px; -} - -.dtb-suggestion-item:hover { - background: var(--background-modifier-hover); -} - -.dtb-suggestion-item.is-selected { +.dtb-api-status-dot.loading { background: var(--interactive-accent); - color: var(--text-on-accent); + animation: dtb-pulse 1.5s ease-in-out infinite; } -/* 建议容器样式 */ -.dtb-suggestions-container { - max-height: 200px; - overflow-y: auto; - border: 1px solid var(--background-modifier-border); - border-radius: 6px; - background: var(--background-primary); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); -} - -/* ============================================================================= - 7. 工具类样式 (Utility Classes) - ============================================================================= */ - -/* 弹性布局工具类 */ -.dtb-flex-1 { - flex: 1; -} - -/* ============================================================================= - 8. 动画效果 (Animation Effects) - ============================================================================= */ - -/* 上滑入场动画 */ -@keyframes slideInUp { - from { - opacity: 0; - transform: translateY(20px); - } - - to { +@keyframes dtb-pulse { + 0%, + 100% { opacity: 1; - transform: translateY(0); } -} - -/* 背景项目的分阶段入场动画 */ -.dtb-background-item { - animation: slideInUp 0.2s ease-out; - animation-fill-mode: both; -} - -.dtb-background-item:nth-child(1) { - animation-delay: 0.05s; -} -.dtb-background-item:nth-child(2) { - animation-delay: 0.1s; -} -.dtb-background-item:nth-child(3) { - animation-delay: 0.15s; -} -.dtb-background-item:nth-child(4) { - animation-delay: 0.2s; -} -.dtb-background-item:nth-child(5) { - animation-delay: 0.25s; -} - -/* ============================================================================= - 9. 深色模式优化 (Dark Mode Optimizations) - ============================================================================= */ - -@media (prefers-color-scheme: dark) { - /* 深色模式下的阴影增强 */ - .dtb-background-container { - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); - } - - .dtb-background-container:hover { - box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3); - } - - .dtb-background-item:hover { - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); - } - - .dtb-bg-preview { - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); - } - - .dtb-bg-preview:hover { - box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4); + 50% { + opacity: 0.3; } } /* ============================================================================= - 10. 响应式设计 (Responsive Design) + 响应式设计 (Responsive Design) ============================================================================= */ -/* 平板和手机端适配 */ +/* TODO 平板和手机端适配 */ + @media (max-width: 600px) { - /* 设置视图响应式调整 */ - .dtb-settings-view { - padding: 1.5rem; - margin: 1rem; - border-radius: 12px; - } - - .dtb-settings-view h1 { - font-size: 1.5rem; - margin-bottom: 1.5rem; - } - - .dtb-settings-view h3 { - font-size: 1.2rem; - margin-top: 2rem; - } - - /* 背景项目响应式布局 */ - .dtb-background-item { + .dtb-item { flex-direction: column; align-items: flex-start; gap: 0.6rem; padding: 0.8rem; } - .dtb-bg-actions { + /* 在较窄的屏幕上改为垂直布局 */ + .dtb-item-list-container .setting-item { + flex-direction: column; + align-items: stretch; + gap: 0.8rem; + } + .dtb-item-list-container .setting-item .setting-item-info { + margin-right: 0; + } + .dtb-item-list-container .setting-item .setting-item-control { + justify-content: center; + } + + .dtb-actions { margin-left: 0; width: 100%; justify-content: space-between; } - - .dtb-bg-actions button { + .dtb-actions button { flex: 1; min-width: 0; } - .dtb-bg-preview { - align-self: center; - width: 60px; - height: 60px; - } - /* 拖拽响应式样式 */ .dtb-drag-handle { min-width: 32px; height: 32px; font-size: 1.2em; } + .dtb-hint { + font-size: 0.85em; + padding: 10px 12px; + } + + .dtb-bg-preview { + align-self: center; + width: 60px; + height: 60px; + } .dtb-bg-content { gap: 0.8rem; } - .dtb-drag-hint { - font-size: 0.85em; - padding: 10px 12px; - } - /* 添加背景容器响应式 */ - .dtb-settings-view .dtb-add-bg-container .setting-item-control { + .dtb-add-bg-container .setting-item-control { flex-direction: column; align-items: stretch; } - .dtb-settings-view .dtb-add-bg-container button { + .dtb-add-bg-container button { width: 100%; justify-content: center; } /* 模态窗口响应式样式 */ - .dtb-modal-flex-container, - .dtb-modal-flex-container-center, - .dtb-modal-flex-container-spaced { + .dtb-flex-container, + .dtb-flex-container-center, + .dtb-flex-container-spaced { flex-direction: column; gap: 12px; } - .dtb-modal-button-group { - flex-direction: column; - gap: 8px; - } - .dtb-modal-hint-text, .dtb-modal-description { font-size: 0.8em; padding: 8px; } } + +/* ============================================================================= + 模态框样式 (Modal Styles) + ============================================================================= */ + +/* 通用模态框容器样式 - 限制宽度并居中 */ +.dtb-api-modal-container { + max-width: 800px; + width: 90vw; + margin: 0 auto; + padding: 20px; +} + +/* 模态框文本域样式 */ +.dtb-modal-textarea { + width: 100%; + min-height: 120px; + padding: 12px; + border: 1px solid var(--background-modifier-border); + border-radius: 6px; + background-color: var(--background-primary); + color: var(--text-normal); + font-family: var(--font-monospace); + font-size: var(--font-ui-small); + resize: vertical; + transition: border-color 0.2s ease; +} + +.dtb-modal-textarea:focus { + outline: none; + border-color: var(--interactive-accent); + box-shadow: 0 0 0 2px var(--interactive-accent-hover); +} + +/* 通用模态框容器样式 - 适用于较小的模态框 */ +.dtb-modal-container { + max-width: 500px; + width: 85vw; + margin: 0 auto; + padding: 20px; +} + +/* 模态框在较小屏幕上的响应式适配 */ +@media (max-width: 900px) { + .dtb-api-modal-container { + max-width: 95vw; + padding: 16px; + } + + .dtb-modal-container { + max-width: 90vw; + padding: 16px; + } + + .dtb-api-modal-container h2, + .dtb-modal-container h2 { + font-size: 1.3rem; + margin-bottom: 20px; + } +} + +@media (max-width: 600px) { + .dtb-api-modal-container, + .dtb-modal-container { + max-width: 100vw; + padding: 12px; + } + + .dtb-flex-container-end { + flex-direction: column; + align-items: stretch; + } + + .dtb-api-modal-container h2, + .dtb-modal-container h2 { + font-size: 1.2rem; + margin-bottom: 16px; + } +} + +/* API模态框特定样式优化 */ +.dtb-api-modal-container .dtb-section-container { + margin-bottom: 20px; + padding: 16px; +} + +.dtb-api-modal-container .dtb-field-container { + margin-bottom: 16px; +} + +.dtb-api-modal-container .dtb-field-container label { + display: block; + margin-bottom: 6px; + font-weight: 500; + color: var(--text-normal); +} + +.dtb-api-modal-container .dtb-input, +.dtb-api-modal-container .dtb-dropdown { + width: 100%; + margin-bottom: 8px; +} + +/* 多选容器优化 */ +.dtb-api-modal-container .dtb-multiselect-container { + max-height: 200px; + overflow-y: auto; + border: 1px solid var(--background-modifier-border); + border-radius: 6px; + padding: 8px; + background-color: var(--background-primary); +} + +.dtb-api-modal-container .dtb-checkbox-container { + margin-bottom: 6px; + padding: 4px; + border-radius: 4px; + transition: background-color 0.15s ease; +} + +.dtb-api-modal-container .dtb-checkbox-container:hover { + background-color: var(--background-modifier-hover); +} + +/* 列表行样式优化 */ +.dtb-api-modal-container .dtb-list-row { + margin-bottom: 8px; + align-items: center; +} + +.dtb-api-modal-container .dtb-list-row .dtb-input { + margin-bottom: 0; +} + +/* 按钮容器优化 */ +.dtb-api-modal-container .dtb-flex-container-end { + margin-top: 24px; + padding-top: 16px; + border-top: 1px solid var(--background-modifier-border); +} + +/* Section Header 样式 */ +.dtb-section-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 12px; +} + +/* 文档链接容器样式 */ +.dtb-doc-links { + display: flex; + gap: 8px; + align-items: center; +} + +/* API 文档链接样式 */ +.dtb-doc-link { + font-size: 0.85em; + color: var(--text-accent); + text-decoration: none; + padding: 4px 8px; + border-radius: 6px; + border: 1px solid var(--background-modifier-border); + background: var(--background-secondary); + transition: all 0.2s ease; + white-space: nowrap; +} + +.dtb-doc-link:hover { + background: var(--background-modifier-hover); + border-color: var(--text-accent); + transform: translateY(-1px); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.dtb-doc-link:active { + transform: translateY(0); +}