feat: 🎉 first just work version

This commit is contained in:
sean2077 2025-08-01 20:41:54 +08:00
parent 04fb100c08
commit 676c0c829c
8 changed files with 3316 additions and 171 deletions

105
README.md
View file

@ -1,94 +1,39 @@
# Obsidian Sample Plugin
# Obsidian Dynamic Theme Background Plugin
This is a sample plugin for Obsidian (https://obsidian.md).
## 介绍
This project uses TypeScript to provide type checking and documentation.
The repo depends on the latest plugin API (obsidian.d.ts) in TypeScript Definition format, which contains TSDoc comments describing what it does.
这是一个用于 Obsidian 的插件,可以根据时间段和用户设置动态更改背景。
This sample plugin demonstrates some of the basic functionality the plugin API can do.
- Adds a ribbon icon, which shows a Notice when clicked.
- Adds a command "Open Sample Modal" which opens a Modal.
- Adds a plugin setting tab to the settings page.
- Registers a global click event and output 'click' to the console.
- Registers a global interval which logs 'setInterval' to the console.
## 特性
## First time developing plugins?
- **时间段背景**: 根据时间段自动切换背景
- **手动切换**: 允许用户手动切换背景
- **自定义设置**: 用户可以自定义背景模糊度、亮度、饱和度和颜色
- **支持多种背景类型**: 包括图片、颜色背景和渐变背景,其中图片背景支持远程图片和仓库本地图片
Quick starting guide for new plugin devs:
![](assets/image.png)
![](assets/image2.png)
- Check if [someone already developed a plugin for what you want](https://obsidian.md/plugins)! There might be an existing plugin similar enough that you can partner up with.
- Make a copy of this repo as a template with the "Use this template" button (login to GitHub if you don't see it).
- Clone your repo to a local development folder. For convenience, you can place this folder in your `.obsidian/plugins/your-plugin-name` folder.
- Install NodeJS, then run `npm i` in the command line under your repo folder.
- Run `npm run dev` to compile your plugin from `main.ts` to `main.js`.
- Make changes to `main.ts` (or create new `.ts` files). Those changes should be automatically compiled into `main.js`.
- Reload Obsidian to load the new version of your plugin.
- Enable plugin in settings window.
- For updates to the Obsidian API run `npm update` in the command line under your repo folder.
## 安装
## Releasing new releases
1. 下载插件
2. 将其放入 Obsidian 的插件文件夹中
3. 在 Obsidian 中启用插件
- Update your `manifest.json` with your new version number, such as `1.0.1`, and the minimum Obsidian version required for your latest release.
- Update your `versions.json` file with `"new-plugin-version": "minimum-obsidian-version"` so older versions of Obsidian can download an older version of your plugin that's compatible.
- Create new GitHub release using your new version number as the "Tag version". Use the exact version number, don't include a prefix `v`. See here for an example: https://github.com/obsidianmd/obsidian-sample-plugin/releases
- Upload the files `manifest.json`, `main.js`, `styles.css` as binary attachments. Note: The manifest.json file must be in two places, first the root path of your repository and also in the release.
- Publish the release.
## 使用
> You can simplify the version bump process by running `npm version patch`, `npm version minor` or `npm version major` after updating `minAppVersion` manually in `manifest.json`.
> The command will bump version in `manifest.json` and `package.json`, and add the entry for the new version to `versions.json`
- 在设置中配置插件
- 使用命令面板切换背景
## Adding your plugin to the community plugin list
## 贡献
- Check the [plugin guidelines](https://docs.obsidian.md/Plugins/Releasing/Plugin+guidelines).
- Publish an initial version.
- Make sure you have a `README.md` file in the root of your repo.
- Make a pull request at https://github.com/obsidianmd/obsidian-releases to add your plugin.
欢迎提交问题和功能请求!如果您想贡献代码,请遵循以下步骤:
## How to use
1. Fork 本仓库
2. 创建新分支
3. 提交更改
4. 创建 Pull Request
- Clone this repo.
- Make sure your NodeJS is at least v16 (`node --version`).
- `npm i` or `yarn` to install dependencies.
- `npm run dev` to start compilation in watch mode.
## 许可证
## Manually installing the plugin
- Copy over `main.js`, `styles.css`, `manifest.json` to your vault `VaultFolder/.obsidian/plugins/your-plugin-id/`.
## Improve code quality with eslint (optional)
- [ESLint](https://eslint.org/) is a tool that analyzes your code to quickly find problems. You can run ESLint against your plugin to find common bugs and ways to improve your code.
- To use eslint with this project, make sure to install eslint from terminal:
- `npm install -g eslint`
- To use eslint to analyze this project use this command:
- `eslint main.ts`
- eslint will then create a report with suggestions for code improvement by file and line number.
- If your source code is in a folder, such as `src`, you can use eslint with this command to analyze all files in that folder:
- `eslint .\src\`
## Funding URL
You can include funding URLs where people who use your plugin can financially support it.
The simple way is to set the `fundingUrl` field to your link in your `manifest.json` file:
```json
{
"fundingUrl": "https://buymeacoffee.com"
}
```
If you have multiple URLs, you can also do:
```json
{
"fundingUrl": {
"Buy Me a Coffee": "https://buymeacoffee.com",
"GitHub Sponsor": "https://github.com/sponsors",
"Patreon": "https://www.patreon.com/"
}
}
```
## API Documentation
See https://github.com/obsidianmd/obsidian-api
本项目采用 MIT 许可证,详情请参阅 LICENSE 文件。

BIN
assets/image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 KiB

BIN
assets/image2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

887
main.ts
View file

@ -1,85 +1,250 @@
import { App, Editor, MarkdownView, Modal, Notice, Plugin, PluginSettingTab, Setting } from 'obsidian';
import { App, Modal, Notice, Plugin, PluginSettingTab, Setting } from 'obsidian';
const fs = require('fs');
// Remember to rename these classes and interfaces!
interface DTBSettings {
enabled: boolean;
interface MyPluginSettings {
mySetting: string;
// 统一的背景模糊度、亮度和饱和度变量、背景颜色
blurDepth: number; // 默认模糊度
brightness4Bg: number; // 默认亮度
saturate4Bg: number; // 默认饱和度
bgColor: string; // 默认背景颜色
mode: 'time-based' | 'interval' | 'manual';
timeRules: TimeRule[];
intervalMinutes: number;
backgrounds: BackgroundItem[];
currentIndex: number; // 当前背景索引
}
const DEFAULT_SETTINGS: MyPluginSettings = {
mySetting: 'default'
interface TimeRule {
id: string;
name: string;
startTime: string; // "HH:MM" format
endTime: string; // "HH:MM" format
backgroundId: string;
enabled: boolean;
}
export default class MyPlugin extends Plugin {
settings: MyPluginSettings;
interface BackgroundItem {
id: string;
name: string;
type: 'image' | 'color' | 'gradient';
value: string; // image URL, color code, or gradient CSS
preview?: string;
}
const DEFAULT_SETTINGS: DTBSettings = {
enabled: false,
blurDepth: 0, // 默认模糊度
brightness4Bg: 0.9, // 默认亮度
saturate4Bg: 1, // 默认饱和度
bgColor: '#1f1e1e80', // 默认背景颜色
mode: 'time-based',
timeRules: [
{
id: 'morning',
name: '早晨',
startTime: '06:00',
endTime: '09:00',
backgroundId: 'default-morning',
enabled: true
},
{
id: 'later-morning',
name: '上午',
startTime: '09:00',
endTime: '11:00',
backgroundId: 'default-later-morning',
enabled: true
},
{
id: 'noon',
name: '中午',
startTime: '11:00',
endTime: '13:00',
backgroundId: 'default-noon',
enabled: true
},
{
id: 'afternoon',
name: '下午',
startTime: '13:00',
endTime: '17:00',
backgroundId: 'default-afternoon',
enabled: true
},
{
id: 'dusk',
name: '黄昏',
startTime: '17:00',
endTime: '18:00',
backgroundId: 'default-dusk',
enabled: true
},
{
id: 'evening',
name: '傍晚',
startTime: '18:00',
endTime: '22:00',
backgroundId: 'default-evening',
enabled: true
},
{
id: 'night',
name: '夜晚',
startTime: '22:00',
endTime: '06:00',
backgroundId: 'default-night',
enabled: true
}
],
intervalMinutes: 60,
backgrounds: [
// 添加默认背景,便于测试
{
id: 'default-morning',
name: '默认早晨背景',
type: 'gradient',
value: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
},
{
id: 'default-later-morning',
name: '默认上午背景',
type: 'gradient',
value: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)'
},
{
id: 'default-noon',
name: '默认中午背景',
type: 'gradient',
value: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)'
},
{
id: 'default-afternoon',
name: '默认下午背景',
type: 'gradient',
value: 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)'
},
{
id: 'default-dusk',
name: '默认黄昏背景',
type: 'gradient',
value: 'linear-gradient(135deg, #fa709a 0%, #fee140 100%)'
},
{
id: 'default-evening',
name: '默认傍晚背景',
type: 'gradient',
value: 'linear-gradient(135deg, #a8edea 0%, #fed6e3 100%)'
},
{
id: 'default-night',
name: '默认夜晚背景',
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() {
await this.loadSettings();
// This creates an icon in the left ribbon.
const ribbonIconEl = this.addRibbonIcon('dice', 'Sample Plugin', (evt: MouseEvent) => {
// Called when the user clicks the icon.
new Notice('This is a notice!');
});
// Perform additional things with the ribbon
ribbonIconEl.addClass('my-plugin-ribbon-class');
// 打印当前设置
console.log('Dynamic Theme Background plugin settings:', this.settings);
// This adds a status bar item to the bottom of the app. Does not work on mobile apps.
const statusBarItemEl = this.addStatusBarItem();
statusBarItemEl.setText('Status Bar Text');
// 创建样式元素
this.styleTag = document.createElement('style');
this.styleTag.id = 'dtb-dynamic-styles';
document.head.appendChild(this.styleTag);
// This adds a simple command that can be triggered anywhere
// 添加设置面板
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.styleTag?.remove();
console.log('Dynamic Theme Background plugin unloaded');
}
addCommands() {
this.addCommand({
id: 'open-sample-modal-simple',
name: 'Open sample modal (simple)',
id: 'toggle-dtb',
name: '开启/关闭动态背景',
callback: () => {
new SampleModal(this.app).open();
}
});
// This adds an editor command that can perform some operation on the current editor instance
this.addCommand({
id: 'sample-editor-command',
name: 'Sample editor command',
editorCallback: (editor: Editor, view: MarkdownView) => {
console.log(editor.getSelection());
editor.replaceSelection('Sample Editor Command');
}
});
// This adds a complex command that can check whether the current state of the app allows execution of the command
this.addCommand({
id: 'open-sample-modal-complex',
name: 'Open sample modal (complex)',
checkCallback: (checking: boolean) => {
// Conditions to check
const markdownView = this.app.workspace.getActiveViewOfType(MarkdownView);
if (markdownView) {
// If checking is true, we're simply "checking" if the command can be run.
// If checking is false, then we want to actually perform the operation.
if (!checking) {
new SampleModal(this.app).open();
}
this.settings.enabled = !this.settings.enabled;
this.saveSettings();
// This command will only show up in Command Palette when the check function returns true
return true;
if (this.settings.enabled) {
this.startBackgroundManager();
new Notice('动态背景已开启');
} else {
this.stopBackgroundManager();
new Notice('动态背景已关闭');
}
}
});
// This adds a settings tab so the user can configure various aspects of the plugin
this.addSettingTab(new SampleSettingTab(this.app, this));
// If the plugin hooks up any global DOM events (on parts of the app that doesn't belong to this plugin)
// Using this function will automatically remove the event listener when this plugin is disabled.
this.registerDomEvent(document, 'click', (evt: MouseEvent) => {
console.log('click', evt);
this.addCommand({
id: 'next-background',
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.updateBackground();
this.saveSettings();
new Notice(`背景已切换到: ${this.background.name}`);
}
}
});
// When registering intervals, this function will automatically clear the interval when the plugin is disabled.
this.registerInterval(window.setInterval(() => console.log('setInterval'), 5 * 60 * 1000));
}
onunload() {
this.addCommand({
id: 'test-current-background',
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.updateBackground();
new Notice(`当前时间段: ${rule.name}, 背景: ${bg.name}`);
} else {
new Notice(`时间段 ${rule.name} 没有设置背景`);
}
} else {
new Notice('当前时间没有匹配的规则');
}
}
});
}
async loadSettings() {
@ -89,46 +254,614 @@ export default class MyPlugin extends Plugin {
async saveSettings() {
await this.saveData(this.settings);
}
// 将图片路径转换为可用的 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: 图片文件 ${imagePath} 不存在`);
return 'none';
}
const p = this.app.vault.getResourcePath(file);
if (!p) {
console.warn(`DTB: 无法获取图片 ${imagePath} 的资源路径`);
return 'none';
}
console.log(`DTB: 使用图片 ${imagePath} 的资源路径 ${p}`);
return `url(${p})`; // 形如 app://local/path/to/image.jpg
}
updateBackground() {
if (!this.settings.enabled || !this.background) {
console.warn('DTB: 背景更新被禁用或没有设置背景');
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 里可能会覆盖已有主题的样式,考虑更好的解法方案
this.styleTag.innerText = `
.dtb-enabled {
--background-primary: ${this.settings.bgColor};
--background-primary-alt: ${this.settings.bgColor};
--background-secondary: ${this.settings.bgColor};
--background-secondary-alt: ${this.settings.bgColor};
--tab-background-active: transparent;
--tab-outline-width: transparent;
}
.dtb-enabled .workspace::before {
${backgroundProperty}: ${cssValue};
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;
}
startBackgroundManager() {
this.stopBackgroundManager();
// 如果没有 'dtb-enabled' 类,则添加
if (!document.body.classList.contains('dtb-enabled')) {
document.body.classList.add('dtb-enabled');
}
const updateBackground = () => {
if (!this.settings.enabled) return;
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;
// 调试信息
console.log('DTB: Current time rule', rule);
console.log('DTB: Found background', this.background);
}
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();
}
break;
}
this.updateBackground();
};
// 立即执行一次
updateBackground();
// 设置定时器
const intervalMs = this.settings.mode === 'time-based' ? 60000 : // 每分钟检查一次
this.settings.intervalMinutes * 60000;
this.intervalId = this.registerInterval(window.setInterval(() => {
updateBackground();
}, 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 SampleModal extends Modal {
constructor(app: App) {
// 模态窗口
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;
contentEl.setText('Woah!');
const { contentEl } = this;
contentEl.createEl('h2', { text: `添加${this.type === 'image' ? '图片' : this.type === 'color' ? '颜色' : '渐变'}背景` });
// Name input
contentEl.createEl('label', { text: '背景名称:' });
this.nameInput = contentEl.createEl('input', { type: 'text' });
this.nameInput.style.width = '100%';
this.nameInput.style.marginBottom = '10px';
// Value input
let valueLabel = '';
let placeholder = '';
switch (this.type) {
case 'image':
valueLabel = '图片的URL(可以是远程图片链接, 也可以是 vault 内部图片的路径):';
placeholder = 'https://example.com/image.jpg 或 path/to/image.jpg';
break;
case 'color':
valueLabel = '颜色值 (如 #ff0000):';
placeholder = '#ffffff';
break;
case 'gradient':
valueLabel = 'CSS 渐变 (如 linear-gradient(45deg, #ff0000, #0000ff)):';
placeholder = 'linear-gradient(45deg, #ff0000, #0000ff)';
break;
}
contentEl.createEl('label', { text: valueLabel });
this.valueInput = contentEl.createEl('input', { type: 'text', placeholder });
this.valueInput.style.width = '100%';
this.valueInput.style.marginBottom = '20px';
// Buttons
const buttonContainer = contentEl.createDiv();
buttonContainer.style.display = 'flex';
buttonContainer.style.justifyContent = 'flex-end';
const cancelButton = buttonContainer.createEl('button', { text: '取消' });
cancelButton.onclick = () => this.close();
const submitButton = buttonContainer.createEl('button', { text: '确定' });
submitButton.style.marginLeft = '10px';
submitButton.onclick = () => {
this.onSubmit(this.nameInput.value, this.valueInput.value || placeholder);
this.close();
};
}
onClose() {
const {contentEl} = this;
const { contentEl } = this;
contentEl.empty();
}
}
class SampleSettingTab extends PluginSettingTab {
plugin: MyPlugin;
class TimeRuleModal extends Modal {
rule: TimeRule;
onSubmit: (rule: { name: string, startTime: string, endTime: string }) => void;
nameInput: HTMLInputElement;
startTimeInput: HTMLInputElement;
endTimeInput: HTMLInputElement;
constructor(app: App, plugin: MyPlugin) {
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: '编辑时间规则' });
// Name input
contentEl.createEl('label', { text: '规则名称:' });
this.nameInput = contentEl.createEl('input', { type: 'text', value: this.rule.name });
this.nameInput.style.width = '100%';
this.nameInput.style.marginBottom = '10px';
// Start time input
contentEl.createEl('label', { text: '开始时间 (HH:MM):' });
this.startTimeInput = contentEl.createEl('input', { type: 'time', value: this.rule.startTime });
this.startTimeInput.style.width = '100%';
this.startTimeInput.style.marginBottom = '10px';
// End time input
contentEl.createEl('label', { text: '结束时间 (HH:MM):' });
this.endTimeInput = contentEl.createEl('input', { type: 'time', value: this.rule.endTime });
this.endTimeInput.style.width = '100%';
this.endTimeInput.style.marginBottom = '20px';
// Buttons
const buttonContainer = contentEl.createDiv();
buttonContainer.style.display = 'flex';
buttonContainer.style.justifyContent = 'flex-end';
const cancelButton = buttonContainer.createEl('button', { text: '取消' });
cancelButton.onclick = () => this.close();
const submitButton = buttonContainer.createEl('button', { text: '确定' });
submitButton.style.marginLeft = '10px';
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 DTBSettingTab extends PluginSettingTab {
plugin: DynamicThemeBackgroundPlugin;
constructor(app: App, plugin: DynamicThemeBackgroundPlugin) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const {containerEl} = this;
const { containerEl } = this;
containerEl.empty();
containerEl.createEl('h2', { text: 'Dynamic Theme Background Settings' });
// 基础设置
new Setting(containerEl)
.setName('Setting #1')
.setDesc('It\'s a secret')
.addText(text => text
.setPlaceholder('Enter your secret')
.setValue(this.plugin.settings.mySetting)
.setName('启用动态背景')
.setDesc('开启或关闭动态背景功能')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.enabled)
.onChange(async (value) => {
this.plugin.settings.mySetting = value;
this.plugin.settings.enabled = value;
await this.plugin.saveSettings();
if (value) {
this.plugin.startBackgroundManager();
} else {
this.plugin.stopBackgroundManager();
}
}));
// 统一的背景模糊度设置
new Setting(containerEl)
.setName('背景模糊度')
.setDesc('设置背景模糊度,范围从 0 到 30')
.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.updateBackground();
}))
.addExtraButton(button => button
.setIcon('reset')
.setTooltip('恢复默认模糊度')
.onClick(async () => {
this.plugin.settings.blurDepth = DEFAULT_SETTINGS.blurDepth; // 恢复默认值
await this.plugin.saveSettings();
this.plugin.updateBackground();
this.display();
}))
;
// 统一的背景亮度设置
new Setting(containerEl)
.setName('背景亮度')
.setDesc('设置背景亮度,范围从 0 到 1.5')
.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.updateBackground();
}))
.addExtraButton(button => button
.setIcon('reset')
.setTooltip('恢复默认亮度')
.onClick(async () => {
this.plugin.settings.brightness4Bg = DEFAULT_SETTINGS.brightness4Bg; // 恢复默认值
await this.plugin.saveSettings();
this.plugin.updateBackground();
this.display();
}))
;
// 统一的背景饱和度设置
new Setting(containerEl)
.setName('背景饱和度')
.setDesc('设置背景饱和度,范围从 0 到 2')
.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.updateBackground();
}))
.addExtraButton(button => button
.setIcon('reset')
.setTooltip('恢复默认饱和度')
.onClick(async () => {
this.plugin.settings.saturate4Bg = DEFAULT_SETTINGS.saturate4Bg; // 恢复默认值
await this.plugin.saveSettings();
this.plugin.updateBackground();
this.display();
}))
;
// 统一的背景颜色设置
new Setting(containerEl)
.setName('背景颜色')
.setDesc('设置背景颜色')
.addColorPicker(colorPicker => colorPicker
.setValue(this.plugin.settings.bgColor)
.onChange(async (value: string) => {
this.plugin.settings.bgColor = value;
await this.plugin.saveSettings();
this.plugin.updateBackground();
}))
.addExtraButton(button => button
.setIcon('reset')
.setTooltip('恢复默认背景颜色')
.onClick(async () => {
this.plugin.settings.bgColor = DEFAULT_SETTINGS.bgColor; // 恢复默认值
await this.plugin.saveSettings();
this.plugin.updateBackground();
this.display();
}))
;
// 背景管理
containerEl.createEl('h3', { text: '背景管理' });
const addBgContainer = containerEl.createDiv('dtb-add-bg-container');
new Setting(addBgContainer)
.setName('添加新背景')
.addButton(button => button
.setButtonText('添加图片背景')
.onClick(() => this.showAddBackgroundModal('image')))
.addButton(button => button
.setButtonText('添加颜色背景')
.onClick(() => this.showAddBackgroundModal('color')))
.addButton(button => button
.setButtonText('添加渐变背景')
.onClick(() => this.showAddBackgroundModal('gradient')));
const backgroundContainer = containerEl.createDiv('dtb-background-container');
this.displayBackgrounds(backgroundContainer);
// 模式设置
containerEl.createEl('h3', { text: '模式设置' });
new Setting(containerEl)
.setName('切换模式')
.setDesc('选择背景切换的方式')
.addDropdown(dropdown => dropdown
.addOption('time-based', '按时间段')
.addOption('interval', '按时间间隔')
.addOption('manual', '手动切换')
.setValue(this.plugin.settings.mode)
.onChange(async (value: 'time-based' | 'interval' | 'manual') => {
this.plugin.settings.mode = value;
await this.plugin.saveSettings();
this.plugin.stopBackgroundManager();
this.plugin.startBackgroundManager();
this.display();
}));
// 时间规则(仅在时间模式下显示)
if (this.plugin.settings.mode === 'time-based') {
containerEl.createEl('h4', { text: '时间规则' });
// 添加一个恢复默认的按钮
new Setting(containerEl)
.setName('恢复默认时间规则')
.setDesc('将时间规则恢复为默认设置')
.addButton(button => button
.setButtonText('恢复默认')
.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('切换间隔')
.setDesc('每隔多少分钟切换一次背景')
.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.createSpan({ text: bg.name, cls: 'dtb-bg-name' });
bgEl.createSpan({ text: bg.type, cls: 'dtb-bg-type' });
// 预览
const preview = bgEl.createDiv('dtb-bg-preview');
if (bg.type === 'image') {
preview.style.backgroundImage = this.plugin.sanitizeImagePath(bg.value);
preview.style.backgroundSize = 'cover';
} else {
preview.style.background = bg.value;
}
// 操作按钮
const actions = bgEl.createDiv('dtb-bg-actions');
actions.createEl('button', { text: '预览' })
.onclick = () => {
this.plugin.background = bg;
this.plugin.updateBackground();
};
actions.createEl('button', { text: '删除' })
.onclick = async () => {
// 使用 filter 方法删除
this.plugin.settings.backgrounds = this.plugin.settings.backgrounds.filter(b => b.id !== bg.id);
await this.plugin.saveSettings();
this.display();
};
});
}
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('', '选择背景');
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('编辑')
.onClick(() => this.showEditTimeRuleModal(rule)))
.addButton(button => button
.setButtonText('删除')
.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();
}
}

View file

@ -1,11 +1,10 @@
{
"id": "sample-plugin",
"name": "Sample Plugin",
"id": "dynamic-theme-background",
"name": "Dynamic Theme Background",
"version": "1.0.0",
"minAppVersion": "0.15.0",
"description": "Demonstrates some of the capabilities of the Obsidian API.",
"author": "Obsidian",
"authorUrl": "https://obsidian.md",
"fundingUrl": "https://obsidian.md/pricing",
"description": "Automatically change theme backgrounds based on time schedules or intervals. Support custom time rules and multiple background types.",
"author": "sean2077",
"authorUrl": "https://github.com/sean2077",
"isDesktopOnly": false
}

2406
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
{
"name": "obsidian-sample-plugin",
"name": "obsidian-dynamic-theme-background-plugin",
"version": "1.0.0",
"description": "This is a sample plugin for Obsidian (https://obsidian.md)",
"description": "A plugin for Obsidian that dynamically changes the background based on time of day and user settings.",
"main": "main.js",
"scripts": {
"dev": "node esbuild.config.mjs",
@ -21,4 +21,4 @@
"tslib": "2.4.0",
"typescript": "4.7.4"
}
}
}

View file

@ -1,8 +1,70 @@
/*
/* Dynamic Theme Background Plugin Styles */
This CSS file will be included with your plugin, and
available in the app when your plugin is enabled.
.dtb-background-container {
margin: 1rem 0;
border: 1px solid var(--background-modifier-border);
border-radius: 6px;
padding: 1rem;
}
If your plugin does not need CSS, delete this file.
.dtb-background-item {
display: flex;
align-items: center;
gap: 1rem;
padding: 0.5rem;
border-bottom: 1px solid var(--background-modifier-border);
margin-bottom: 0.5rem;
}
*/
.dtb-background-item:last-child {
border-bottom: none;
margin-bottom: 0;
}
.dtb-bg-name {
font-weight: 500;
flex: 1;
}
.dtb-bg-type {
background: var(--interactive-accent);
color: var(--text-on-accent);
padding: 0.2rem 0.5rem;
border-radius: 4px;
font-size: 0.8rem;
}
.dtb-bg-preview {
width: 40px;
height: 40px;
border-radius: 4px;
border: 1px solid var(--background-modifier-border);
background-size: cover;
background-position: center;
}
.dtb-bg-actions {
display: flex;
gap: 0.5rem;
}
.dtb-bg-actions button {
padding: 0.3rem 0.6rem;
border-radius: 4px;
border: 1px solid var(--interactive-normal);
background: var(--interactive-normal);
color: var(--text-normal);
cursor: pointer;
font-size: 0.8rem;
}
.dtb-bg-actions button:hover {
background: var(--interactive-hover);
}
/* 时间规则样式 */
.setting-item.mod-toggle .setting-item-control {
display: flex;
align-items: center;
gap: 0.5rem;
}