mirror of
https://github.com/fantasy-ke/obsidian-cf-imgbed.git
synced 2026-07-22 06:43:08 +00:00
feat: 添加水印和压缩功能,优化设置界面和验证逻辑
- 新增水印和压缩处理的客户端功能 - 增加设置验证逻辑,确保配置有效性 - 优化设置界面,增加选项卡和样式 - 更新类型定义,包含新配置项 - 添加文件类型和大小验证 - 实现文件上传前的水印和压缩处理
This commit is contained in:
parent
236a9d62d1
commit
b834fdb5c3
8 changed files with 1052 additions and 10 deletions
22
copy-to-mobile.bat
Normal file
22
copy-to-mobile.bat
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
@echo off
|
||||
echo 正在复制文件到移动设备...
|
||||
echo.
|
||||
|
||||
REM 请修改下面的路径为你的 Obsidian 库路径
|
||||
set OBSIDIAN_PATH="../Doc/.obsidian\plugins\cf-imageBed"
|
||||
|
||||
REM 创建插件目录(如果不存在)
|
||||
if not exist %OBSIDIAN_PATH% (
|
||||
mkdir %OBSIDIAN_PATH%
|
||||
)
|
||||
|
||||
REM 复制文件
|
||||
copy main.js %OBSIDIAN_PATH%\
|
||||
copy manifest.json %OBSIDIAN_PATH%\
|
||||
copy styles.css %OBSIDIAN_PATH%\
|
||||
|
||||
echo.
|
||||
echo 文件复制完成!
|
||||
echo 请在 Obsidian 中重新加载插件或重启应用
|
||||
echo.
|
||||
pause
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import { App, PluginSettingTab, Setting } from 'obsidian';
|
||||
import { CFImageBedSettings } from '../types';
|
||||
import { SettingsValidator } from './settingsValidator';
|
||||
|
||||
export class CFImageBedSettingTab extends PluginSettingTab {
|
||||
plugin: any;
|
||||
|
|
@ -13,11 +14,60 @@ export class CFImageBedSettingTab extends PluginSettingTab {
|
|||
const {containerEl} = this;
|
||||
|
||||
containerEl.empty();
|
||||
|
||||
// 添加插件专用的CSS类名,限制样式作用域
|
||||
containerEl.addClass('cf-imagebed-settings');
|
||||
|
||||
containerEl.createEl('h2', { text: 'CF ImageBed 设置' });
|
||||
|
||||
// 创建选项卡容器
|
||||
const tabContainer = containerEl.createDiv('cf-imagebed-tabs');
|
||||
const tabContent = containerEl.createDiv('cf-imagebed-tab-content');
|
||||
|
||||
// 创建选项卡按钮
|
||||
const basicTab = tabContainer.createEl('button', { text: '基础设置', cls: 'cf-tab-button active' });
|
||||
const advancedTab = tabContainer.createEl('button', { text: '高级设置', cls: 'cf-tab-button' });
|
||||
const userTab = tabContainer.createEl('button', { text: '用户体验', cls: 'cf-tab-button' });
|
||||
const backupTab = tabContainer.createEl('button', { text: '备份设置', cls: 'cf-tab-button' });
|
||||
|
||||
// 创建选项卡内容区域
|
||||
const basicContent = tabContent.createDiv('cf-tab-panel active');
|
||||
const advancedContent = tabContent.createDiv('cf-tab-panel');
|
||||
const userContent = tabContent.createDiv('cf-tab-panel');
|
||||
const backupContent = tabContent.createDiv('cf-tab-panel');
|
||||
|
||||
// 选项卡切换逻辑
|
||||
const switchTab = (activeTab: HTMLElement, activeContent: HTMLElement) => {
|
||||
// 移除所有活动状态
|
||||
tabContainer.querySelectorAll('.cf-tab-button').forEach(btn => btn.classList.remove('active'));
|
||||
tabContent.querySelectorAll('.cf-tab-panel').forEach(panel => panel.classList.remove('active'));
|
||||
|
||||
// 添加活动状态
|
||||
activeTab.classList.add('active');
|
||||
activeContent.classList.add('active');
|
||||
};
|
||||
|
||||
basicTab.addEventListener('click', () => switchTab(basicTab, basicContent));
|
||||
advancedTab.addEventListener('click', () => switchTab(advancedTab, advancedContent));
|
||||
userTab.addEventListener('click', () => switchTab(userTab, userContent));
|
||||
backupTab.addEventListener('click', () => switchTab(backupTab, backupContent));
|
||||
|
||||
// 基础设置选项卡内容
|
||||
this.createBasicSettings(basicContent);
|
||||
|
||||
// 高级设置选项卡内容
|
||||
this.createAdvancedSettings(advancedContent);
|
||||
|
||||
// 用户体验选项卡内容
|
||||
this.createUserExperienceSettings(userContent);
|
||||
|
||||
// 备份设置选项卡内容
|
||||
this.createBackupSettings(backupContent);
|
||||
}
|
||||
|
||||
private createBasicSettings(container: HTMLElement): void {
|
||||
// API URL 设置
|
||||
new Setting(containerEl)
|
||||
new Setting(container)
|
||||
.setName('API URL')
|
||||
.setDesc('CloudFlare ImgBed 的 API 地址(例如:https://your.domain)')
|
||||
.addText(text => text
|
||||
|
|
@ -29,7 +79,7 @@ export class CFImageBedSettingTab extends PluginSettingTab {
|
|||
}));
|
||||
|
||||
// 认证码设置
|
||||
new Setting(containerEl)
|
||||
new Setting(container)
|
||||
.setName('认证码')
|
||||
.setDesc('上传认证码')
|
||||
.addText(text => text
|
||||
|
|
@ -41,7 +91,7 @@ export class CFImageBedSettingTab extends PluginSettingTab {
|
|||
}));
|
||||
|
||||
// 上传渠道设置
|
||||
new Setting(containerEl)
|
||||
new Setting(container)
|
||||
.setName('上传渠道')
|
||||
.setDesc('选择上传渠道')
|
||||
.addDropdown(dropdown => dropdown
|
||||
|
|
@ -55,7 +105,7 @@ export class CFImageBedSettingTab extends PluginSettingTab {
|
|||
}));
|
||||
|
||||
// 文件命名方式设置
|
||||
new Setting(containerEl)
|
||||
new Setting(container)
|
||||
.setName('文件命名方式')
|
||||
.setDesc('选择文件命名方式')
|
||||
.addDropdown(dropdown => dropdown
|
||||
|
|
@ -70,7 +120,7 @@ export class CFImageBedSettingTab extends PluginSettingTab {
|
|||
}));
|
||||
|
||||
// 返回格式设置
|
||||
new Setting(containerEl)
|
||||
new Setting(container)
|
||||
.setName('返回链接格式')
|
||||
.setDesc('选择返回链接格式')
|
||||
.addDropdown(dropdown => dropdown
|
||||
|
|
@ -83,7 +133,7 @@ export class CFImageBedSettingTab extends PluginSettingTab {
|
|||
}));
|
||||
|
||||
// 上传目录设置
|
||||
new Setting(containerEl)
|
||||
new Setting(container)
|
||||
.setName('上传目录')
|
||||
.setDesc('上传目录,用相对路径表示(例如:img/test)')
|
||||
.addText(text => text
|
||||
|
|
@ -95,7 +145,7 @@ export class CFImageBedSettingTab extends PluginSettingTab {
|
|||
}));
|
||||
|
||||
// 服务端压缩设置
|
||||
new Setting(containerEl)
|
||||
new Setting(container)
|
||||
.setName('服务端压缩')
|
||||
.setDesc('启用服务端压缩(仅针对 Telegram 渠道的图片文件)')
|
||||
.addToggle(toggle => toggle
|
||||
|
|
@ -106,7 +156,7 @@ export class CFImageBedSettingTab extends PluginSettingTab {
|
|||
}));
|
||||
|
||||
// 自动重试设置
|
||||
new Setting(containerEl)
|
||||
new Setting(container)
|
||||
.setName('自动重试')
|
||||
.setDesc('失败时自动切换渠道重试')
|
||||
.addToggle(toggle => toggle
|
||||
|
|
@ -116,4 +166,238 @@ export class CFImageBedSettingTab extends PluginSettingTab {
|
|||
await this.plugin.saveSettings();
|
||||
}));
|
||||
}
|
||||
|
||||
private createAdvancedSettings(container: HTMLElement): void {
|
||||
// 文件大小限制
|
||||
new Setting(container)
|
||||
.setName('最大文件大小')
|
||||
.setDesc('设置上传文件的最大大小(MB)')
|
||||
.addSlider(slider => slider
|
||||
.setLimits(1, 50, 1)
|
||||
.setValue(this.plugin.settings.maxFileSize)
|
||||
.setDynamicTooltip()
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.maxFileSize = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
// 允许的文件类型
|
||||
new Setting(container)
|
||||
.setName('允许的文件类型')
|
||||
.setDesc('设置允许上传的文件类型(用逗号分隔)')
|
||||
.addText(text => text
|
||||
.setPlaceholder('jpg,jpeg,png,gif,webp,bmp')
|
||||
.setValue(this.plugin.settings.allowedFileTypes.join(','))
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.allowedFileTypes = value.split(',').map(t => t.trim());
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
// 水印设置
|
||||
new Setting(container)
|
||||
.setName('启用水印')
|
||||
.setDesc('为上传的图片添加水印')
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.enableWatermark)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.enableWatermark = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
// 水印文字
|
||||
new Setting(container)
|
||||
.setName('水印文字')
|
||||
.setDesc('设置水印文字内容')
|
||||
.addText(text => text
|
||||
.setPlaceholder('水印文字')
|
||||
.setValue(this.plugin.settings.watermarkText)
|
||||
.setDisabled(!this.plugin.settings.enableWatermark)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.watermarkText = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
// 水印位置
|
||||
new Setting(container)
|
||||
.setName('水印位置')
|
||||
.setDesc('设置水印在图片中的位置')
|
||||
.addDropdown(dropdown => dropdown
|
||||
.addOption('top-left', '左上角')
|
||||
.addOption('top-right', '右上角')
|
||||
.addOption('bottom-left', '左下角')
|
||||
.addOption('bottom-right', '右下角')
|
||||
.addOption('center', '居中')
|
||||
.setValue(this.plugin.settings.watermarkPosition)
|
||||
.setDisabled(!this.plugin.settings.enableWatermark)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.watermarkPosition = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
// 水印字体大小
|
||||
new Setting(container)
|
||||
.setName('水印字体大小')
|
||||
.setDesc('设置水印文字的字体大小(像素)')
|
||||
.addSlider(slider => slider
|
||||
.setLimits(12, 72, 2)
|
||||
.setValue(this.plugin.settings.watermarkSize)
|
||||
.setDynamicTooltip()
|
||||
.setDisabled(!this.plugin.settings.enableWatermark)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.watermarkSize = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
// 水印透明度
|
||||
new Setting(container)
|
||||
.setName('水印透明度')
|
||||
.setDesc('设置水印的透明度(0-1)')
|
||||
.addSlider(slider => slider
|
||||
.setLimits(0.1, 1, 0.1)
|
||||
.setValue(this.plugin.settings.watermarkOpacity)
|
||||
.setDynamicTooltip()
|
||||
.setDisabled(!this.plugin.settings.enableWatermark)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.watermarkOpacity = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
// 客户端压缩设置
|
||||
new Setting(container)
|
||||
.setName('启用客户端压缩')
|
||||
.setDesc('在上传前自动压缩图片以减少文件大小')
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.enableClientCompress)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.enableClientCompress = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
// 压缩阈值
|
||||
new Setting(container)
|
||||
.setName('压缩阈值')
|
||||
.setDesc('设置图片大小阈值,超过此值将自动压缩(MB)')
|
||||
.addSlider(slider => slider
|
||||
.setLimits(0.5, 10, 0.5)
|
||||
.setValue(this.plugin.settings.compressThreshold)
|
||||
.setDynamicTooltip()
|
||||
.setDisabled(!this.plugin.settings.enableClientCompress)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.compressThreshold = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
// 期望大小
|
||||
new Setting(container)
|
||||
.setName('期望大小')
|
||||
.setDesc('设置压缩后图片大小期望值(MB)')
|
||||
.addSlider(slider => slider
|
||||
.setLimits(0.1, 5, 0.1)
|
||||
.setValue(this.plugin.settings.targetSize)
|
||||
.setDynamicTooltip()
|
||||
.setDisabled(!this.plugin.settings.enableClientCompress)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.targetSize = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
}
|
||||
|
||||
private createUserExperienceSettings(container: HTMLElement): void {
|
||||
// 显示上传进度
|
||||
new Setting(container)
|
||||
.setName('显示上传进度')
|
||||
.setDesc('在上传过程中显示进度条')
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.showUploadProgress)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.showUploadProgress = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
// 显示成功通知
|
||||
new Setting(container)
|
||||
.setName('显示成功通知')
|
||||
.setDesc('上传成功后显示通知消息')
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.showSuccessNotification)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.showSuccessNotification = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
// 显示错误通知
|
||||
new Setting(container)
|
||||
.setName('显示错误通知')
|
||||
.setDesc('上传失败时显示错误消息')
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.showErrorNotification)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.showErrorNotification = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
// 通知持续时间
|
||||
new Setting(container)
|
||||
.setName('通知持续时间')
|
||||
.setDesc('设置通知消息显示的持续时间(秒)')
|
||||
.addSlider(slider => slider
|
||||
.setLimits(1, 10, 1)
|
||||
.setValue(this.plugin.settings.notificationDuration)
|
||||
.setDynamicTooltip()
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.notificationDuration = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
// 快捷键设置
|
||||
new Setting(container)
|
||||
.setName('启用快捷键')
|
||||
.setDesc('启用快捷键快速上传图片')
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.enableHotkey)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.enableHotkey = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
// 快捷键
|
||||
new Setting(container)
|
||||
.setName('快捷键')
|
||||
.setDesc('设置上传图片的快捷键')
|
||||
.addText(text => text
|
||||
.setPlaceholder('Ctrl+Shift+U')
|
||||
.setValue(this.plugin.settings.hotkey)
|
||||
.setDisabled(!this.plugin.settings.enableHotkey)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.hotkey = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
}
|
||||
|
||||
private createBackupSettings(container: HTMLElement): void {
|
||||
// 启用本地备份
|
||||
new Setting(container)
|
||||
.setName('启用本地备份')
|
||||
.setDesc('在上传到云端的同时,在本地保存一份备份')
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.enableLocalBackup)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.enableLocalBackup = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
// 备份路径
|
||||
new Setting(container)
|
||||
.setName('备份路径')
|
||||
.setDesc('设置本地备份的存储路径(相对于库根目录)')
|
||||
.addText(text => text
|
||||
.setPlaceholder('attachments/backup')
|
||||
.setValue(this.plugin.settings.backupPath)
|
||||
.setDisabled(!this.plugin.settings.enableLocalBackup)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.backupPath = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
103
src/settings/settingsValidator.ts
Normal file
103
src/settings/settingsValidator.ts
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
import { Notice } from 'obsidian';
|
||||
import { CFImageBedSettings } from '../types';
|
||||
|
||||
export class SettingsValidator {
|
||||
static validateSettings(settings: CFImageBedSettings): { isValid: boolean; errors: string[] } {
|
||||
const errors: string[] = [];
|
||||
|
||||
// 验证基础配置
|
||||
if (!settings.apiUrl) {
|
||||
errors.push('API URL 不能为空');
|
||||
} else if (!this.isValidUrl(settings.apiUrl)) {
|
||||
errors.push('API URL 格式不正确');
|
||||
}
|
||||
|
||||
if (!settings.authCode) {
|
||||
errors.push('认证码不能为空');
|
||||
}
|
||||
|
||||
// 验证文件大小
|
||||
if (settings.maxFileSize < 1 || settings.maxFileSize > 100) {
|
||||
errors.push('文件大小限制应在 1-100 MB 之间');
|
||||
}
|
||||
|
||||
// 验证客户端压缩配置
|
||||
if (settings.enableClientCompress) {
|
||||
if (settings.compressThreshold < 0.1 || settings.compressThreshold > 20) {
|
||||
errors.push('压缩阈值应在 0.1-20 MB 之间');
|
||||
}
|
||||
|
||||
if (settings.targetSize < 0.1 || settings.targetSize > 10) {
|
||||
errors.push('期望大小应在 0.1-10 MB 之间');
|
||||
}
|
||||
|
||||
if (settings.targetSize >= settings.compressThreshold) {
|
||||
errors.push('期望大小应小于压缩阈值');
|
||||
}
|
||||
}
|
||||
|
||||
// 验证通知持续时间
|
||||
if (settings.notificationDuration < 1 || settings.notificationDuration > 30) {
|
||||
errors.push('通知持续时间应在 1-30 秒之间');
|
||||
}
|
||||
|
||||
// 验证文件类型
|
||||
if (settings.allowedFileTypes.length === 0) {
|
||||
errors.push('至少需要指定一种允许的文件类型');
|
||||
}
|
||||
|
||||
// 验证水印设置
|
||||
if (settings.enableWatermark) {
|
||||
if (!settings.watermarkText.trim()) {
|
||||
errors.push('启用水印时必须设置水印文字');
|
||||
}
|
||||
|
||||
if (settings.watermarkSize < 8 || settings.watermarkSize > 100) {
|
||||
errors.push('水印字体大小应在 8-100 像素之间');
|
||||
}
|
||||
|
||||
if (settings.watermarkOpacity < 0.1 || settings.watermarkOpacity > 1) {
|
||||
errors.push('水印透明度应在 0.1-1 之间');
|
||||
}
|
||||
}
|
||||
|
||||
// 验证备份路径
|
||||
if (settings.enableLocalBackup && !settings.backupPath.trim()) {
|
||||
errors.push('启用本地备份时必须设置备份路径');
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: errors.length === 0,
|
||||
errors
|
||||
};
|
||||
}
|
||||
|
||||
static isValidUrl(url: string): boolean {
|
||||
try {
|
||||
new URL(url);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static validateFileType(fileName: string, allowedTypes: string[]): boolean {
|
||||
const extension = fileName.split('.').pop()?.toLowerCase();
|
||||
return extension ? allowedTypes.includes(extension) : false;
|
||||
}
|
||||
|
||||
static validateFileSize(fileSize: number, maxSizeMB: number): boolean {
|
||||
const maxSizeBytes = maxSizeMB * 1024 * 1024;
|
||||
return fileSize <= maxSizeBytes;
|
||||
}
|
||||
|
||||
static showValidationErrors(errors: string[]): void {
|
||||
if (errors.length > 0) {
|
||||
new Notice(`配置验证失败:\n${errors.join('\n')}`, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
static showValidationSuccess(): void {
|
||||
new Notice('配置验证通过', 2000);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
export interface CFImageBedSettings {
|
||||
// 基础配置
|
||||
apiUrl: string;
|
||||
authCode: string;
|
||||
uploadChannel: string;
|
||||
|
|
@ -7,9 +8,38 @@ export interface CFImageBedSettings {
|
|||
uploadFolder: string;
|
||||
serverCompress: boolean;
|
||||
autoRetry: boolean;
|
||||
|
||||
// 高级配置
|
||||
maxFileSize: number; // MB
|
||||
allowedFileTypes: string[];
|
||||
enableWatermark: boolean;
|
||||
watermarkText: string;
|
||||
watermarkPosition: string;
|
||||
watermarkSize: number; // 水印字体大小
|
||||
watermarkOpacity: number; // 水印透明度 0-1
|
||||
|
||||
// 客户端压缩配置
|
||||
enableClientCompress: boolean;
|
||||
compressThreshold: number; // MB - 压缩阈值
|
||||
targetSize: number; // MB - 期望大小
|
||||
|
||||
// 用户体验配置
|
||||
showUploadProgress: boolean;
|
||||
showSuccessNotification: boolean;
|
||||
showErrorNotification: boolean;
|
||||
notificationDuration: number; // 秒
|
||||
|
||||
// 备份配置
|
||||
enableLocalBackup: boolean;
|
||||
backupPath: string;
|
||||
|
||||
// 快捷键配置
|
||||
enableHotkey: boolean;
|
||||
hotkey: string;
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: CFImageBedSettings = {
|
||||
// 基础配置
|
||||
apiUrl: '',
|
||||
authCode: '',
|
||||
uploadChannel: 'telegram',
|
||||
|
|
@ -17,5 +47,33 @@ export const DEFAULT_SETTINGS: CFImageBedSettings = {
|
|||
returnFormat: 'default',
|
||||
uploadFolder: '',
|
||||
serverCompress: true,
|
||||
autoRetry: true
|
||||
autoRetry: true,
|
||||
|
||||
// 高级配置
|
||||
maxFileSize: 10, // 10MB
|
||||
allowedFileTypes: ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp'],
|
||||
enableWatermark: false,
|
||||
watermarkText: '',
|
||||
watermarkPosition: 'bottom-right',
|
||||
watermarkSize: 24, // 字体大小
|
||||
watermarkOpacity: 0.7, // 透明度
|
||||
|
||||
// 客户端压缩配置
|
||||
enableClientCompress: false,
|
||||
compressThreshold: 2, // 2MB
|
||||
targetSize: 1, // 1MB
|
||||
|
||||
// 用户体验配置
|
||||
showUploadProgress: false,
|
||||
showSuccessNotification: true,
|
||||
showErrorNotification: false,
|
||||
notificationDuration: 5,
|
||||
|
||||
// 备份配置
|
||||
enableLocalBackup: false,
|
||||
backupPath: '',
|
||||
|
||||
// 快捷键配置
|
||||
enableHotkey: false,
|
||||
hotkey: 'Ctrl+Shift+U'
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import { Notice } from 'obsidian';
|
||||
import { CFImageBedSettings } from '../types';
|
||||
import { ClientCompressor } from '../utils/clientCompressor';
|
||||
import { ClientWatermark } from '../utils/clientWatermark';
|
||||
|
||||
export class UploadService {
|
||||
constructor(private settings: CFImageBedSettings) {}
|
||||
|
|
@ -11,8 +13,51 @@ export class UploadService {
|
|||
}
|
||||
|
||||
try {
|
||||
// 检查文件类型
|
||||
if (!this.isAllowedFileType(file)) {
|
||||
new Notice(`不支持的文件类型: ${file.type}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 检查文件大小
|
||||
if (!this.isFileSizeAllowed(file)) {
|
||||
new Notice(`文件大小超过限制: ${ClientCompressor.formatFileSize(file.size)}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 客户端处理(水印 + 压缩)
|
||||
let processedFile = file;
|
||||
|
||||
// 1. 添加水印
|
||||
if (this.settings.enableWatermark && ClientWatermark.isWatermarkable(file)) {
|
||||
console.log('CF ImageBed: 开始添加水印');
|
||||
processedFile = await ClientWatermark.addWatermark(
|
||||
processedFile,
|
||||
this.settings.watermarkText,
|
||||
this.settings.watermarkPosition,
|
||||
this.settings.watermarkSize,
|
||||
this.settings.watermarkOpacity
|
||||
);
|
||||
}
|
||||
|
||||
// 2. 客户端压缩
|
||||
if (this.settings.enableClientCompress && ClientCompressor.isCompressible(processedFile)) {
|
||||
console.log('CF ImageBed: 开始客户端压缩处理');
|
||||
processedFile = await ClientCompressor.compressImage(
|
||||
processedFile,
|
||||
this.settings.targetSize,
|
||||
this.settings.compressThreshold
|
||||
);
|
||||
|
||||
// 显示压缩结果
|
||||
const originalSize = ClientCompressor.formatFileSize(file.size);
|
||||
const processedSize = ClientCompressor.formatFileSize(processedFile.size);
|
||||
console.log(`CF ImageBed: 处理完成 - 原始: ${originalSize}, 处理后: ${processedSize}`);
|
||||
}
|
||||
|
||||
// 创建上传参数
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('file', processedFile);
|
||||
|
||||
const params = new URLSearchParams({
|
||||
authCode: this.settings.authCode,
|
||||
|
|
@ -27,6 +72,7 @@ export class UploadService {
|
|||
params.append('uploadFolder', this.settings.uploadFolder);
|
||||
}
|
||||
|
||||
// 上传文件
|
||||
const response = await fetch(`${this.settings.apiUrl}/upload?${params}`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
|
|
@ -56,4 +102,20 @@ export class UploadService {
|
|||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查文件类型是否允许
|
||||
*/
|
||||
private isAllowedFileType(file: File): boolean {
|
||||
const extension = file.name.split('.').pop()?.toLowerCase();
|
||||
return extension ? this.settings.allowedFileTypes.includes(extension) : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查文件大小是否允许
|
||||
*/
|
||||
private isFileSizeAllowed(file: File): boolean {
|
||||
const maxSizeBytes = this.settings.maxFileSize * 1024 * 1024;
|
||||
return file.size <= maxSizeBytes;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
164
src/utils/clientCompressor.ts
Normal file
164
src/utils/clientCompressor.ts
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
import { Notice } from 'obsidian';
|
||||
|
||||
export class ClientCompressor {
|
||||
/**
|
||||
* 压缩图片文件
|
||||
* @param file 原始图片文件
|
||||
* @param targetSizeMB 目标大小(MB)
|
||||
* @param thresholdMB 压缩阈值(MB)
|
||||
* @returns 压缩后的文件或原始文件
|
||||
*/
|
||||
static async compressImage(
|
||||
file: File,
|
||||
targetSizeMB: number,
|
||||
thresholdMB: number
|
||||
): Promise<File> {
|
||||
// 检查文件大小是否超过阈值
|
||||
const fileSizeMB = file.size / (1024 * 1024);
|
||||
|
||||
if (fileSizeMB <= thresholdMB) {
|
||||
console.log(`CF ImageBed: 文件大小 ${fileSizeMB.toFixed(2)}MB 未超过阈值 ${thresholdMB}MB,跳过压缩`);
|
||||
return file;
|
||||
}
|
||||
|
||||
console.log(`CF ImageBed: 开始压缩图片,原始大小: ${fileSizeMB.toFixed(2)}MB,目标大小: ${targetSizeMB}MB`);
|
||||
|
||||
try {
|
||||
// 创建图片对象
|
||||
const img = new Image();
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
if (!ctx) {
|
||||
throw new Error('无法创建画布上下文');
|
||||
}
|
||||
|
||||
// 等待图片加载
|
||||
await new Promise((resolve, reject) => {
|
||||
img.onload = resolve;
|
||||
img.onerror = reject;
|
||||
img.src = URL.createObjectURL(file);
|
||||
});
|
||||
|
||||
// 计算压缩后的尺寸
|
||||
const { width, height } = this.calculateCompressedDimensions(
|
||||
img.width,
|
||||
img.height,
|
||||
targetSizeMB,
|
||||
fileSizeMB
|
||||
);
|
||||
|
||||
// 设置画布尺寸
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
|
||||
// 绘制压缩后的图片
|
||||
ctx.drawImage(img, 0, 0, width, height);
|
||||
|
||||
// 转换为 Blob
|
||||
const compressedBlob = await new Promise<Blob>((resolve) => {
|
||||
canvas.toBlob((blob) => {
|
||||
if (blob) {
|
||||
resolve(blob);
|
||||
} else {
|
||||
throw new Error('压缩失败');
|
||||
}
|
||||
}, 'image/jpeg', 0.8); // 使用 JPEG 格式,质量 0.8
|
||||
});
|
||||
|
||||
// 创建新的文件对象
|
||||
const compressedFile = new File([compressedBlob], file.name, {
|
||||
type: 'image/jpeg',
|
||||
lastModified: Date.now()
|
||||
});
|
||||
|
||||
const compressedSizeMB = compressedFile.size / (1024 * 1024);
|
||||
console.log(`CF ImageBed: 压缩完成,压缩后大小: ${compressedSizeMB.toFixed(2)}MB`);
|
||||
|
||||
// 清理资源
|
||||
URL.revokeObjectURL(img.src);
|
||||
|
||||
return compressedFile;
|
||||
|
||||
} catch (error) {
|
||||
console.error('CF ImageBed: 图片压缩失败:', error);
|
||||
new Notice('图片压缩失败,将上传原始文件', 3000);
|
||||
return file;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算压缩后的图片尺寸
|
||||
* @param originalWidth 原始宽度
|
||||
* @param originalHeight 原始高度
|
||||
* @param targetSizeMB 目标大小(MB)
|
||||
* @param originalSizeMB 原始大小(MB)
|
||||
* @returns 压缩后的尺寸
|
||||
*/
|
||||
private static calculateCompressedDimensions(
|
||||
originalWidth: number,
|
||||
originalHeight: number,
|
||||
targetSizeMB: number,
|
||||
originalSizeMB: number
|
||||
): { width: number; height: number } {
|
||||
// 计算压缩比例(基于文件大小)
|
||||
const sizeRatio = Math.sqrt(targetSizeMB / originalSizeMB);
|
||||
|
||||
// 计算新尺寸
|
||||
let newWidth = Math.floor(originalWidth * sizeRatio);
|
||||
let newHeight = Math.floor(originalHeight * sizeRatio);
|
||||
|
||||
// 确保尺寸不会太小(最小 100px)
|
||||
const minSize = 100;
|
||||
if (newWidth < minSize || newHeight < minSize) {
|
||||
const aspectRatio = originalWidth / originalHeight;
|
||||
if (aspectRatio > 1) {
|
||||
newWidth = Math.max(minSize, newWidth);
|
||||
newHeight = Math.floor(newWidth / aspectRatio);
|
||||
} else {
|
||||
newHeight = Math.max(minSize, newHeight);
|
||||
newWidth = Math.floor(newHeight * aspectRatio);
|
||||
}
|
||||
}
|
||||
|
||||
// 确保尺寸不会太大(最大 4000px)
|
||||
const maxSize = 4000;
|
||||
if (newWidth > maxSize || newHeight > maxSize) {
|
||||
const aspectRatio = originalWidth / originalHeight;
|
||||
if (aspectRatio > 1) {
|
||||
newWidth = Math.min(maxSize, newWidth);
|
||||
newHeight = Math.floor(newWidth / aspectRatio);
|
||||
} else {
|
||||
newHeight = Math.min(maxSize, newHeight);
|
||||
newWidth = Math.floor(newHeight * aspectRatio);
|
||||
}
|
||||
}
|
||||
|
||||
return { width: newWidth, height: newHeight };
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查文件类型是否支持压缩
|
||||
* @param file 文件对象
|
||||
* @returns 是否支持压缩
|
||||
*/
|
||||
static isCompressible(file: File): boolean {
|
||||
const compressibleTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp'];
|
||||
return compressibleTypes.includes(file.type.toLowerCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件大小描述
|
||||
* @param bytes 字节数
|
||||
* @returns 格式化的文件大小
|
||||
*/
|
||||
static formatFileSize(bytes: number): string {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
}
|
||||
178
src/utils/clientWatermark.ts
Normal file
178
src/utils/clientWatermark.ts
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
import { Notice } from 'obsidian';
|
||||
|
||||
export class ClientWatermark {
|
||||
/**
|
||||
* 为图片添加水印
|
||||
* @param file 原始图片文件
|
||||
* @param watermarkText 水印文字
|
||||
* @param position 水印位置
|
||||
* @param fontSize 字体大小
|
||||
* @param opacity 透明度
|
||||
* @returns 带水印的图片文件
|
||||
*/
|
||||
static async addWatermark(
|
||||
file: File,
|
||||
watermarkText: string,
|
||||
position: string,
|
||||
fontSize: number,
|
||||
opacity: number
|
||||
): Promise<File> {
|
||||
if (!watermarkText.trim()) {
|
||||
console.log('CF ImageBed: 水印文字为空,跳过水印处理');
|
||||
return file;
|
||||
}
|
||||
|
||||
console.log(`CF ImageBed: 开始添加水印 - 文字: ${watermarkText}, 位置: ${position}`);
|
||||
|
||||
try {
|
||||
// 创建图片对象
|
||||
const img = new Image();
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
if (!ctx) {
|
||||
throw new Error('无法创建画布上下文');
|
||||
}
|
||||
|
||||
// 等待图片加载
|
||||
await new Promise((resolve, reject) => {
|
||||
img.onload = resolve;
|
||||
img.onerror = reject;
|
||||
img.src = URL.createObjectURL(file);
|
||||
});
|
||||
|
||||
// 设置画布尺寸
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
|
||||
// 绘制原始图片
|
||||
ctx.drawImage(img, 0, 0);
|
||||
|
||||
// 设置水印样式
|
||||
ctx.font = `bold ${fontSize}px Arial, sans-serif`;
|
||||
ctx.fillStyle = `rgba(255, 255, 255, ${opacity})`;
|
||||
ctx.strokeStyle = `rgba(0, 0, 0, ${opacity * 0.5})`;
|
||||
ctx.lineWidth = 2;
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
|
||||
// 计算水印位置
|
||||
const positionData = this.calculateWatermarkPosition(
|
||||
img.width,
|
||||
img.height,
|
||||
position,
|
||||
fontSize,
|
||||
watermarkText
|
||||
);
|
||||
|
||||
// 绘制水印文字(带描边效果)
|
||||
ctx.strokeText(watermarkText, positionData.x, positionData.y);
|
||||
ctx.fillText(watermarkText, positionData.x, positionData.y);
|
||||
|
||||
// 转换为 Blob
|
||||
const watermarkedBlob = await new Promise<Blob>((resolve) => {
|
||||
canvas.toBlob((blob) => {
|
||||
if (blob) {
|
||||
resolve(blob);
|
||||
} else {
|
||||
throw new Error('水印添加失败');
|
||||
}
|
||||
}, file.type, 0.9);
|
||||
});
|
||||
|
||||
// 创建新的文件对象
|
||||
const watermarkedFile = new File([watermarkedBlob], file.name, {
|
||||
type: file.type,
|
||||
lastModified: Date.now()
|
||||
});
|
||||
|
||||
console.log('CF ImageBed: 水印添加完成');
|
||||
|
||||
// 清理资源
|
||||
URL.revokeObjectURL(img.src);
|
||||
|
||||
return watermarkedFile;
|
||||
|
||||
} catch (error) {
|
||||
console.error('CF ImageBed: 水印添加失败:', error);
|
||||
new Notice('水印添加失败,将上传原始文件', 3000);
|
||||
return file;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算水印位置
|
||||
* @param imgWidth 图片宽度
|
||||
* @param imgHeight 图片高度
|
||||
* @param position 位置字符串
|
||||
* @param fontSize 字体大小
|
||||
* @param text 水印文字
|
||||
* @returns 水印位置坐标
|
||||
*/
|
||||
private static calculateWatermarkPosition(
|
||||
imgWidth: number,
|
||||
imgHeight: number,
|
||||
position: string,
|
||||
fontSize: number,
|
||||
text: string
|
||||
): { x: number; y: number } {
|
||||
const padding = fontSize; // 边距
|
||||
const textWidth = text.length * fontSize * 0.6; // 估算文字宽度
|
||||
const textHeight = fontSize;
|
||||
|
||||
let x: number, y: number;
|
||||
|
||||
switch (position) {
|
||||
case 'top-left':
|
||||
x = padding + textWidth / 2;
|
||||
y = padding + textHeight / 2;
|
||||
break;
|
||||
case 'top-right':
|
||||
x = imgWidth - padding - textWidth / 2;
|
||||
y = padding + textHeight / 2;
|
||||
break;
|
||||
case 'bottom-left':
|
||||
x = padding + textWidth / 2;
|
||||
y = imgHeight - padding - textHeight / 2;
|
||||
break;
|
||||
case 'bottom-right':
|
||||
x = imgWidth - padding - textWidth / 2;
|
||||
y = imgHeight - padding - textHeight / 2;
|
||||
break;
|
||||
case 'center':
|
||||
x = imgWidth / 2;
|
||||
y = imgHeight / 2;
|
||||
break;
|
||||
default:
|
||||
// 默认右下角
|
||||
x = imgWidth - padding - textWidth / 2;
|
||||
y = imgHeight - padding - textHeight / 2;
|
||||
}
|
||||
|
||||
return { x, y };
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否支持添加水印
|
||||
* @param file 文件对象
|
||||
* @returns 是否支持水印
|
||||
*/
|
||||
static isWatermarkable(file: File): boolean {
|
||||
const watermarkableTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp'];
|
||||
return watermarkableTypes.includes(file.type.toLowerCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取水印位置选项
|
||||
* @returns 位置选项数组
|
||||
*/
|
||||
static getPositionOptions(): Array<{ value: string; label: string }> {
|
||||
return [
|
||||
{ value: 'top-left', label: '左上角' },
|
||||
{ value: 'top-right', label: '右上角' },
|
||||
{ value: 'bottom-left', label: '左下角' },
|
||||
{ value: 'bottom-right', label: '右下角' },
|
||||
{ value: 'center', label: '居中' }
|
||||
];
|
||||
}
|
||||
}
|
||||
171
styles.css
171
styles.css
|
|
@ -6,3 +6,174 @@ available in the app when your plugin is enabled.
|
|||
If your plugin does not need CSS, delete this file.
|
||||
|
||||
*/
|
||||
|
||||
/* CF ImageBed 插件专用样式 - 限制作用域避免影响全局 */
|
||||
|
||||
/* 设置界面选项卡样式 - 仅在插件设置页面生效 */
|
||||
.cf-imagebed-settings .cf-imagebed-tabs {
|
||||
display: flex;
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
margin-bottom: 1rem;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.cf-imagebed-settings .cf-tab-button {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
cursor: pointer;
|
||||
color: var(--text-muted);
|
||||
border-bottom: 2px solid transparent;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.cf-imagebed-settings .cf-tab-button:hover {
|
||||
color: var(--text-normal);
|
||||
background: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
.cf-imagebed-settings .cf-tab-button.active {
|
||||
color: var(--text-accent);
|
||||
border-bottom-color: var(--text-accent);
|
||||
background: var(--background-modifier-active);
|
||||
}
|
||||
|
||||
.cf-imagebed-settings .cf-imagebed-tab-content {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.cf-imagebed-settings .cf-tab-panel {
|
||||
display: none;
|
||||
animation: cf-fadeIn 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.cf-imagebed-settings .cf-tab-panel.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@keyframes cf-fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 滑块样式优化 - 仅在插件设置页面生效 */
|
||||
.cf-imagebed-settings .setting-item .slider-container {
|
||||
width: 100%;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.cf-imagebed-settings .setting-item .slider-container input[type="range"] {
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
border-radius: 3px;
|
||||
background: var(--background-modifier-border);
|
||||
outline: none;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
.cf-imagebed-settings .setting-item .slider-container input[type="range"]::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
background: var(--text-accent);
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.cf-imagebed-settings .setting-item .slider-container input[type="range"]::-moz-range-thumb {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
background: var(--text-accent);
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* 按钮样式优化 - 仅在插件设置页面生效 */
|
||||
.cf-imagebed-settings .setting-item .mod-button {
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
background: var(--background-primary);
|
||||
color: var(--text-normal);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.cf-imagebed-settings .setting-item .mod-button:hover {
|
||||
background: var(--background-modifier-hover);
|
||||
border-color: var(--text-accent);
|
||||
}
|
||||
|
||||
.cf-imagebed-settings .setting-item .mod-button.mod-warning {
|
||||
background: var(--text-error);
|
||||
color: var(--text-on-accent);
|
||||
border-color: var(--text-error);
|
||||
}
|
||||
|
||||
.cf-imagebed-settings .setting-item .mod-button.mod-warning:hover {
|
||||
background: var(--text-error-hover);
|
||||
}
|
||||
|
||||
/* 设置项分组样式 - 仅在插件设置页面生效 */
|
||||
.cf-imagebed-settings .setting-item {
|
||||
margin-bottom: 1rem;
|
||||
padding: 0.75rem;
|
||||
border-radius: 6px;
|
||||
background: var(--background-secondary);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.cf-imagebed-settings .setting-item:hover {
|
||||
background: var(--background-modifier-hover);
|
||||
border-color: var(--text-accent);
|
||||
}
|
||||
|
||||
.cf-imagebed-settings .setting-item .setting-item-name {
|
||||
font-weight: 600;
|
||||
color: var(--text-normal);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.cf-imagebed-settings .setting-item .setting-item-description {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* 禁用状态样式 - 仅在插件设置页面生效 */
|
||||
.cf-imagebed-settings .setting-item .setting-item-control input:disabled,
|
||||
.cf-imagebed-settings .setting-item .setting-item-control select:disabled,
|
||||
.cf-imagebed-settings .setting-item .setting-item-control button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* 响应式设计 - 仅在插件设置页面生效 */
|
||||
@media (max-width: 768px) {
|
||||
.cf-imagebed-settings .cf-imagebed-tabs {
|
||||
flex-wrap: wrap;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.cf-imagebed-settings .cf-tab-button {
|
||||
padding: 0.4rem 0.8rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.cf-imagebed-settings .setting-item {
|
||||
padding: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue