mirror of
https://github.com/olcubo/obsidian-cubox.git
synced 2026-07-22 05:43:25 +00:00
initial
This commit is contained in:
parent
494fa9de77
commit
9f00dad5e9
11 changed files with 3387 additions and 144 deletions
|
|
@ -15,7 +15,7 @@ const context = await esbuild.context({
|
|||
banner: {
|
||||
js: banner,
|
||||
},
|
||||
entryPoints: ["main.ts"],
|
||||
entryPoints: ["src/main.ts"],
|
||||
bundle: true,
|
||||
external: [
|
||||
"obsidian",
|
||||
|
|
|
|||
134
main.ts
134
main.ts
|
|
@ -1,134 +0,0 @@
|
|||
import { App, Editor, MarkdownView, Modal, Notice, Plugin, PluginSettingTab, Setting } from 'obsidian';
|
||||
|
||||
// Remember to rename these classes and interfaces!
|
||||
|
||||
interface MyPluginSettings {
|
||||
mySetting: string;
|
||||
}
|
||||
|
||||
const DEFAULT_SETTINGS: MyPluginSettings = {
|
||||
mySetting: 'default'
|
||||
}
|
||||
|
||||
export default class MyPlugin extends Plugin {
|
||||
settings: MyPluginSettings;
|
||||
|
||||
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');
|
||||
|
||||
// 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 adds a simple command that can be triggered anywhere
|
||||
this.addCommand({
|
||||
id: 'open-sample-modal-simple',
|
||||
name: 'Open sample modal (simple)',
|
||||
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 command will only show up in Command Palette when the check function returns true
|
||||
return true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 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);
|
||||
});
|
||||
|
||||
// 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() {
|
||||
|
||||
}
|
||||
|
||||
async loadSettings() {
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
||||
}
|
||||
|
||||
async saveSettings() {
|
||||
await this.saveData(this.settings);
|
||||
}
|
||||
}
|
||||
|
||||
class SampleModal extends Modal {
|
||||
constructor(app: App) {
|
||||
super(app);
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
const {contentEl} = this;
|
||||
contentEl.setText('Woah!');
|
||||
}
|
||||
|
||||
onClose() {
|
||||
const {contentEl} = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
}
|
||||
|
||||
class SampleSettingTab extends PluginSettingTab {
|
||||
plugin: MyPlugin;
|
||||
|
||||
constructor(app: App, plugin: MyPlugin) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
display(): void {
|
||||
const {containerEl} = this;
|
||||
|
||||
containerEl.empty();
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Setting #1')
|
||||
.setDesc('It\'s a secret')
|
||||
.addText(text => text
|
||||
.setPlaceholder('Enter your secret')
|
||||
.setValue(this.plugin.settings.mySetting)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.mySetting = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,10 @@
|
|||
{
|
||||
"id": "sample-plugin",
|
||||
"name": "Sample Plugin",
|
||||
"id": "obsidian-cubox-sync",
|
||||
"name": "Cubox Sync",
|
||||
"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": "同步 Cubox 文章和高亮内容到 Obsidian",
|
||||
"author": "Your Name",
|
||||
"authorUrl": "https://github.com/yourusername",
|
||||
"isDesktopOnly": false
|
||||
}
|
||||
|
|
|
|||
2244
package-lock.json
generated
Normal file
2244
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "obsidian-sample-plugin",
|
||||
"name": "obsidian-cubox",
|
||||
"version": "1.0.0",
|
||||
"description": "This is a sample plugin for Obsidian (https://obsidian.md)",
|
||||
"description": "This is a cubox plugin for Obsidian (https://obsidian.md)",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"dev": "node esbuild.config.mjs",
|
||||
|
|
@ -19,6 +19,8 @@
|
|||
"esbuild": "0.17.3",
|
||||
"obsidian": "latest",
|
||||
"tslib": "2.4.0",
|
||||
"typescript": "4.7.4"
|
||||
"typescript": "4.7.4",
|
||||
"@types/mustache": "^4.2.0",
|
||||
"mustache": "^4.2.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
234
src/cuboxApi.ts
Normal file
234
src/cuboxApi.ts
Normal file
|
|
@ -0,0 +1,234 @@
|
|||
import { Notice } from 'obsidian';
|
||||
import { formatISODateTime } from './utils';
|
||||
|
||||
export interface CuboxArticle {
|
||||
cardId: string;
|
||||
id: string; // 兼容原有接口
|
||||
title: string;
|
||||
description: string;
|
||||
url: string;
|
||||
domain: string;
|
||||
contentId: string;
|
||||
coverKey: string;
|
||||
isArchived: boolean;
|
||||
hasStar: boolean;
|
||||
createTime: string;
|
||||
updateTime: string;
|
||||
content?: string;
|
||||
highlights?: CuboxHighlight[];
|
||||
tags?: string[];
|
||||
folder?: string;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
export interface CuboxHighlight {
|
||||
id: string;
|
||||
text: string;
|
||||
note?: string;
|
||||
color?: string;
|
||||
createDate: string;
|
||||
}
|
||||
|
||||
export interface CuboxApiOptions {
|
||||
domain: string;
|
||||
apiKey: string;
|
||||
}
|
||||
|
||||
export interface CuboxFolder {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface ListResponse {
|
||||
code: number;
|
||||
message: string;
|
||||
data: {
|
||||
list: CuboxArticle[];
|
||||
};
|
||||
}
|
||||
|
||||
interface ContentResponse {
|
||||
code: number;
|
||||
message: string;
|
||||
data: string;
|
||||
}
|
||||
|
||||
interface FoldersResponse {
|
||||
code: number;
|
||||
message: string;
|
||||
data: {
|
||||
list: CuboxFolder[];
|
||||
};
|
||||
}
|
||||
|
||||
export class CuboxApi {
|
||||
private endpoint: string;
|
||||
private apiKey: string;
|
||||
|
||||
constructor(options: CuboxApiOptions) {
|
||||
this.endpoint = `https://${options.domain}`;
|
||||
this.apiKey = options.apiKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试 API 连接是否有效
|
||||
*/
|
||||
async testConnection(): Promise<boolean> {
|
||||
try {
|
||||
// 尝试获取一篇文章来测试连接
|
||||
// const result = await this.getArticles(1, 1);
|
||||
new Notice('测试 Cubox 连接成功');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Cubox API 连接测试失败:', error);
|
||||
new Notice('Cubox API 连接失败,请检查域名和 API Key');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async request(path: string, options: RequestInit = {}) {
|
||||
const url = `${this.endpoint}${path}`;
|
||||
const headers = {
|
||||
'Authorization': `Bearer ${this.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
headers: {
|
||||
...headers,
|
||||
...options.headers,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`API request failed: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文章列表
|
||||
* @param folders 文件夹过滤数组
|
||||
* @param type 类型过滤
|
||||
* @param status 状态过滤 'all' | 'read' | 'starred' | 'annotated'
|
||||
* @param lastCardId 上一页最后一篇文章的ID
|
||||
*/
|
||||
async getArticles(
|
||||
folders: string[] = [],
|
||||
type?: string,
|
||||
status?: string,
|
||||
lastCardId: string | null = null,
|
||||
pageSize: number = 50
|
||||
): Promise<{
|
||||
articles: CuboxArticle[],
|
||||
hasMore: boolean,
|
||||
lastCardId: string | null
|
||||
}> {
|
||||
try {
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (lastCardId !== null && lastCardId.length > 0) {
|
||||
searchParams.append('lastCardId', lastCardId);
|
||||
}
|
||||
searchParams.append('pageSize', pageSize.toString());
|
||||
|
||||
// 如果有选择文件夹,添加文件夹过滤参数
|
||||
if (folders && folders.length > 0) {
|
||||
searchParams.append('folders', folders.join(','));
|
||||
}
|
||||
|
||||
// 添加状态过滤参数
|
||||
if (status && status !== 'all') {
|
||||
switch (status) {
|
||||
case 'read':
|
||||
searchParams.append('isRead', 'true');
|
||||
break;
|
||||
case 'starred':
|
||||
searchParams.append('hasStar', 'true');
|
||||
break;
|
||||
case 'annotated':
|
||||
searchParams.append('hasAnnotation', 'true');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const path = `/c/api/third-party/card/list?${searchParams.toString()}`;
|
||||
const response = await this.request(path) as ListResponse;
|
||||
|
||||
const articles = response.data.list.map(article => {
|
||||
// 添加兼容字段,并格式化日期
|
||||
return {
|
||||
...article,
|
||||
id: article.cardId,
|
||||
createDate: article.createTime ? formatISODateTime(article.createTime) : '',
|
||||
// 其他可能需要的兼容字段
|
||||
};
|
||||
});
|
||||
|
||||
const hasMore = articles.length >= pageSize;
|
||||
const newLastCardId = articles.length > 0 ? articles[articles.length - 1].cardId : null;
|
||||
|
||||
return {
|
||||
articles,
|
||||
hasMore,
|
||||
lastCardId: newLastCardId
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('获取 Cubox 文章失败:', error);
|
||||
new Notice('获取 Cubox 文章失败');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文章详情,包括内容
|
||||
* @param articleId 文章ID
|
||||
*/
|
||||
async getArticleDetail(articleId: string): Promise<string | null> {
|
||||
try {
|
||||
const path = `/c/api/third-party/card/content?cardId=${articleId}`;
|
||||
const response = await this.request(path) as ContentResponse;
|
||||
|
||||
// 直接返回文章内容
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error(`获取文章 ${articleId} 详情失败:`, error);
|
||||
new Notice(`获取文章详情失败`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文章的高亮内容
|
||||
* @param articleId 文章ID
|
||||
*/
|
||||
async getHighlights(articleId: string): Promise<CuboxHighlight[]> {
|
||||
try {
|
||||
// 这里需要实现获取高亮的API调用
|
||||
// 暂时返回空数组
|
||||
return [];
|
||||
} catch (error) {
|
||||
console.error(`获取文章 ${articleId} 高亮内容失败:`, error);
|
||||
new Notice(`获取高亮内容失败`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户的文件夹列表
|
||||
*/
|
||||
async getFolders(): Promise<CuboxFolder[]> {
|
||||
try {
|
||||
const path = '/c/api/third-party/folder/list';
|
||||
const response = await this.request(path) as FoldersResponse;
|
||||
|
||||
return response.data.list || [];
|
||||
} catch (error) {
|
||||
console.error('获取 Cubox 文件夹列表失败:', error);
|
||||
new Notice('获取 Cubox 文件夹列表失败');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
127
src/folderSelectModal.ts
Normal file
127
src/folderSelectModal.ts
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
import { App, Modal, Setting } from 'obsidian';
|
||||
import { CuboxApi } from './cuboxApi';
|
||||
|
||||
export interface CuboxFolder {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export class FolderSelectModal extends Modal {
|
||||
private folders: CuboxFolder[] = [];
|
||||
private selectedFolders: Set<string> = new Set();
|
||||
private onSave: (selectedFolders: string[]) => void;
|
||||
private cuboxApi: CuboxApi;
|
||||
|
||||
constructor(app: App, cuboxApi: CuboxApi, initialSelected: string[], onSave: (selectedFolders: string[]) => void) {
|
||||
super(app);
|
||||
this.cuboxApi = cuboxApi;
|
||||
this.onSave = onSave;
|
||||
|
||||
// 初始化已选择的文件夹
|
||||
initialSelected.forEach(id => {
|
||||
if (id) this.selectedFolders.add(id);
|
||||
});
|
||||
}
|
||||
|
||||
async onOpen() {
|
||||
const { contentEl } = this;
|
||||
|
||||
// 设置标题
|
||||
contentEl.createEl('h2', { text: 'Cubox: 管理要同步的文件夹' });
|
||||
|
||||
try {
|
||||
// 显示加载中提示
|
||||
const loadingEl = contentEl.createEl('p', { text: '加载文件夹列表...' });
|
||||
|
||||
// 获取文件夹列表
|
||||
this.folders = await this.cuboxApi.getFolders();
|
||||
|
||||
// 移除加载提示
|
||||
loadingEl.remove();
|
||||
|
||||
// 如果没有文件夹,显示提示
|
||||
if (this.folders.length === 0) {
|
||||
contentEl.createEl('p', { text: '没有找到文件夹' });
|
||||
return;
|
||||
}
|
||||
|
||||
// 添加"全选"选项
|
||||
new Setting(contentEl)
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.isAllSelected())
|
||||
.onChange(value => {
|
||||
if (value) {
|
||||
// 全选
|
||||
this.folders.forEach(folder => this.selectedFolders.add(folder.id));
|
||||
} else {
|
||||
// 全不选
|
||||
this.selectedFolders.clear();
|
||||
}
|
||||
// 刷新界面
|
||||
this.redraw();
|
||||
}))
|
||||
.setName('全部文件夹');
|
||||
|
||||
// 创建文件夹列表
|
||||
this.createFolderList();
|
||||
|
||||
// 添加底部按钮
|
||||
const footerEl = contentEl.createDiv('modal-footer');
|
||||
|
||||
// 取消按钮
|
||||
const cancelButton = footerEl.createEl('button', { text: '取消' });
|
||||
cancelButton.addEventListener('click', () => {
|
||||
this.close();
|
||||
});
|
||||
|
||||
// 保存按钮
|
||||
const saveButton = footerEl.createEl('button', { text: '保存', cls: 'mod-cta' });
|
||||
saveButton.addEventListener('click', () => {
|
||||
this.onSave(Array.from(this.selectedFolders));
|
||||
this.close();
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('加载文件夹失败:', error);
|
||||
contentEl.createEl('p', { text: '加载文件夹失败,请检查网络连接和 API 密钥' });
|
||||
}
|
||||
}
|
||||
|
||||
private createFolderList() {
|
||||
const { contentEl } = this;
|
||||
|
||||
// 清除现有列表
|
||||
const existingList = contentEl.querySelector('.folder-list');
|
||||
if (existingList) existingList.remove();
|
||||
|
||||
// 创建新列表容器
|
||||
const listEl = contentEl.createDiv({ cls: 'folder-list' });
|
||||
|
||||
// 添加每个文件夹的选项
|
||||
this.folders.forEach(folder => {
|
||||
new Setting(listEl)
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.selectedFolders.has(folder.id))
|
||||
.onChange(value => {
|
||||
if (value) {
|
||||
this.selectedFolders.add(folder.id);
|
||||
} else {
|
||||
this.selectedFolders.delete(folder.id);
|
||||
}
|
||||
}))
|
||||
.setName(folder.name);
|
||||
});
|
||||
}
|
||||
|
||||
private isAllSelected(): boolean {
|
||||
return this.folders.length > 0 && this.folders.every(folder => this.selectedFolders.has(folder.id));
|
||||
}
|
||||
|
||||
private redraw() {
|
||||
this.createFolderList();
|
||||
}
|
||||
|
||||
onClose() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
}
|
||||
504
src/main.ts
Normal file
504
src/main.ts
Normal file
|
|
@ -0,0 +1,504 @@
|
|||
import { App, Editor, MarkdownView, Modal, Notice, Plugin, PluginSettingTab, Setting, TFolder } from 'obsidian';
|
||||
import { CuboxApi, CuboxApiOptions } from './cuboxApi';
|
||||
import { TemplateProcessor } from './templateProcessor';
|
||||
import { formatISODateTime, getCurrentFormattedTime } from './utils';
|
||||
import { FolderSelectModal } from './folderSelectModal';
|
||||
|
||||
interface CuboxSyncSettings {
|
||||
domain: string;
|
||||
apiKey: string;
|
||||
folderFilter: string[];
|
||||
typeFilter: string;
|
||||
statusFilter: 'all' | 'read' | 'starred' | 'annotated';
|
||||
syncFrequency: number;
|
||||
targetFolder: string;
|
||||
filenameTemplate: string;
|
||||
frontMatterTemplate: string;
|
||||
contentTemplate: string;
|
||||
highlightInContent: boolean;
|
||||
dateFormat: string;
|
||||
lastSyncTime: number;
|
||||
lastSyncCardId: string | null;
|
||||
}
|
||||
|
||||
const DEFAULT_SETTINGS: CuboxSyncSettings = {
|
||||
domain: 'cubox.cc',
|
||||
apiKey: '',
|
||||
folderFilter: [],
|
||||
typeFilter: '',
|
||||
statusFilter: 'all',
|
||||
syncFrequency: 30, // 分钟
|
||||
targetFolder: 'Cubox',
|
||||
filenameTemplate: '{{cardTitle}}-{{createDate}}',
|
||||
frontMatterTemplate: 'title: {{cardTitle}}\nurl: {{url}}\ndate: {{createDate}}',
|
||||
contentTemplate: '# {{cardTitle}}\n\n{{{content}}}\n\n## Highlights\n{{#highlightsList}}\n- {{{text}}}\n{{/highlightsList}}',
|
||||
highlightInContent: true,
|
||||
dateFormat: 'YYYY-MM-dd',
|
||||
lastSyncTime: 0,
|
||||
lastSyncCardId: null
|
||||
}
|
||||
|
||||
export default class CuboxSyncPlugin extends Plugin {
|
||||
settings: CuboxSyncSettings;
|
||||
cuboxApi: CuboxApi;
|
||||
templateProcessor: TemplateProcessor;
|
||||
syncIntervalId: number;
|
||||
|
||||
async onload() {
|
||||
await this.loadSettings();
|
||||
|
||||
// 初始化 API 和模板处理器
|
||||
this.cuboxApi = new CuboxApi({
|
||||
domain: this.settings.domain,
|
||||
apiKey: this.settings.apiKey
|
||||
});
|
||||
this.templateProcessor = new TemplateProcessor();
|
||||
// 设置模板处理器的日期格式
|
||||
this.templateProcessor.setDateFormat(this.settings.dateFormat);
|
||||
|
||||
// 添加左侧图标
|
||||
const ribbonIconEl = this.addRibbonIcon('download', 'Sync Cubox', async (evt: MouseEvent) => {
|
||||
new Notice('开始同步 Cubox 数据...');
|
||||
await this.syncCubox();
|
||||
});
|
||||
ribbonIconEl.addClass('cubox-sync-ribbon-class');
|
||||
|
||||
// 添加状态栏
|
||||
const statusBarItemEl = this.addStatusBarItem();
|
||||
statusBarItemEl.setText(`上次同步: ${this.formatLastSyncTime()}`);
|
||||
|
||||
// 添加同步命令
|
||||
this.addCommand({
|
||||
id: 'sync-cubox-data',
|
||||
name: '同步 Cubox 数据',
|
||||
callback: async () => {
|
||||
await this.syncCubox();
|
||||
}
|
||||
});
|
||||
|
||||
// 添加设置选项卡
|
||||
this.addSettingTab(new CuboxSyncSettingTab(this.app, this));
|
||||
|
||||
// 设置自动同步
|
||||
this.setupAutoSync();
|
||||
|
||||
// 加载样式
|
||||
this.loadStyles();
|
||||
}
|
||||
|
||||
onunload() {
|
||||
// 清除自动同步定时器
|
||||
if (this.syncIntervalId) {
|
||||
window.clearInterval(this.syncIntervalId);
|
||||
}
|
||||
|
||||
// 移除样式
|
||||
const styleEl = document.getElementById('cubox-sync-styles');
|
||||
if (styleEl) styleEl.remove();
|
||||
}
|
||||
|
||||
async loadSettings() {
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
||||
}
|
||||
|
||||
async saveSettings() {
|
||||
await this.saveData(this.settings);
|
||||
|
||||
// 更新 API 配置
|
||||
this.cuboxApi = new CuboxApi({
|
||||
domain: this.settings.domain,
|
||||
apiKey: this.settings.apiKey
|
||||
});
|
||||
|
||||
// 更新模板处理器的日期格式
|
||||
this.templateProcessor.setDateFormat(this.settings.dateFormat);
|
||||
|
||||
// 重新设置自动同步
|
||||
this.setupAutoSync();
|
||||
}
|
||||
|
||||
setupAutoSync() {
|
||||
// 清除现有定时器
|
||||
if (this.syncIntervalId) {
|
||||
window.clearInterval(this.syncIntervalId);
|
||||
}
|
||||
|
||||
// 如果频率大于0,设置新的定时器
|
||||
if (this.settings.syncFrequency > 0) {
|
||||
this.syncIntervalId = window.setInterval(
|
||||
async () => await this.syncCubox(),
|
||||
this.settings.syncFrequency * 60 * 1000
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async syncCubox(continueLastSync: boolean = false) {
|
||||
try {
|
||||
// 检查 API 连接
|
||||
const isConnected = await this.cuboxApi.testConnection();
|
||||
if (!isConnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 确保目标文件夹存在
|
||||
await this.ensureTargetFolder();
|
||||
|
||||
// 如果选择继续上次同步,则使用保存的 lastSyncCardId
|
||||
let lastCardId: string | null = continueLastSync ? this.settings.lastSyncCardId : null;
|
||||
let hasMore = true;
|
||||
let syncCount = 0;
|
||||
|
||||
// 分页获取所有文章
|
||||
while (hasMore) {
|
||||
// 获取文章列表
|
||||
const result = await this.cuboxApi.getArticles(
|
||||
this.settings.folderFilter,
|
||||
this.settings.typeFilter,
|
||||
this.settings.statusFilter,
|
||||
lastCardId
|
||||
);
|
||||
|
||||
const { articles, hasMore: moreArticles, lastCardId: newLastCardId } = result;
|
||||
|
||||
if (articles.length === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
// 处理每篇文章
|
||||
for (const article of articles) {
|
||||
// 获取文章详情和高亮
|
||||
const content = await this.cuboxApi.getArticleDetail(article.cardId);
|
||||
if (content === null) continue;
|
||||
|
||||
// 获取高亮内容
|
||||
const highlights = await this.cuboxApi.getHighlights(article.cardId);
|
||||
|
||||
// 合并文章基本信息、内容和高亮
|
||||
const fullArticle = {
|
||||
...article,
|
||||
content: content,
|
||||
highlights: highlights
|
||||
};
|
||||
|
||||
// 处理文件名和内容
|
||||
const filename = this.templateProcessor.processFilenameTemplate(
|
||||
this.settings.filenameTemplate,
|
||||
fullArticle
|
||||
);
|
||||
|
||||
const frontMatter = this.templateProcessor.processFrontMatterTemplate(
|
||||
this.settings.frontMatterTemplate,
|
||||
fullArticle
|
||||
);
|
||||
|
||||
const contentTemplate = this.templateProcessor.processContentTemplate(
|
||||
this.settings.contentTemplate,
|
||||
fullArticle
|
||||
);
|
||||
|
||||
// 组合最终内容
|
||||
let finalContent = '';
|
||||
if (frontMatter) {
|
||||
finalContent = `---\n${frontMatter}\n---\n\n`;
|
||||
}
|
||||
finalContent += contentTemplate;
|
||||
|
||||
// 创建或更新文件
|
||||
const filePath = `${this.settings.targetFolder}/${filename}.md`;
|
||||
await this.app.vault.adapter.write(filePath, finalContent);
|
||||
|
||||
syncCount++;
|
||||
}
|
||||
|
||||
// 更新分页参数
|
||||
hasMore = moreArticles;
|
||||
lastCardId = newLastCardId;
|
||||
|
||||
// 在每次分页循环后保存当前的同步状态
|
||||
this.settings.lastSyncCardId = lastCardId;
|
||||
this.settings.lastSyncTime = Date.now();
|
||||
await this.saveSettings();
|
||||
|
||||
// 只在 lastCardId 不为 null 时才继续分页
|
||||
if (lastCardId === null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 同步完成后,清除 lastSyncCardId(表示完整同步已完成)
|
||||
this.settings.lastSyncCardId = null;
|
||||
await this.saveSettings();
|
||||
|
||||
new Notice(`成功同步 ${syncCount} 篇 Cubox 文章`);
|
||||
} catch (error) {
|
||||
console.error('同步 Cubox 数据失败:', error);
|
||||
new Notice('同步 Cubox 数据失败,请检查设置和网络连接');
|
||||
}
|
||||
}
|
||||
|
||||
async ensureTargetFolder() {
|
||||
const folderPath = this.settings.targetFolder;
|
||||
|
||||
// 检查文件夹是否存在
|
||||
if (!(await this.app.vault.adapter.exists(folderPath))) {
|
||||
// 创建文件夹
|
||||
await this.app.vault.createFolder(folderPath);
|
||||
}
|
||||
}
|
||||
|
||||
formatLastSyncTime(): string {
|
||||
if (!this.settings.lastSyncTime) {
|
||||
return '从未同步';
|
||||
}
|
||||
|
||||
// 使用新的格式化方法
|
||||
return formatISODateTime(new Date(this.settings.lastSyncTime).toISOString(), 'yyyy-MM-dd HH:mm');
|
||||
}
|
||||
|
||||
private loadStyles() {
|
||||
// 添加自定义样式
|
||||
const styleEl = document.createElement('style');
|
||||
styleEl.id = 'cubox-sync-styles';
|
||||
styleEl.textContent = `
|
||||
/* 文件夹选择模态框样式 */
|
||||
.folder-list {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
margin: 10px 0;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 4px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 20px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.modal-footer button {
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
background-color: var(--interactive-normal);
|
||||
color: var(--text-normal);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.modal-footer button.mod-cta {
|
||||
background-color: var(--interactive-accent);
|
||||
color: var(--text-on-accent);
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(styleEl);
|
||||
}
|
||||
}
|
||||
|
||||
class CuboxSyncSettingTab extends PluginSettingTab {
|
||||
plugin: CuboxSyncPlugin;
|
||||
|
||||
constructor(app: App, plugin: CuboxSyncPlugin) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
display(): void {
|
||||
const {containerEl} = this;
|
||||
|
||||
containerEl.empty();
|
||||
|
||||
// 连接设置部分
|
||||
containerEl.createEl('h2', {text: 'Connect Obsidian to Your Cubox'});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Cubox Server Domain')
|
||||
.setDesc('Select the correct domain name of the Cubox you are using.')
|
||||
.addDropdown(dropdown => dropdown
|
||||
.addOption('cubox.cc', 'cubox.cc')
|
||||
.setValue(this.plugin.settings.domain)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.domain = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Your Cubox API Key')
|
||||
.setDesc('You can create a key in the settings of Cubox web app.')
|
||||
.addText(text => text
|
||||
.setPlaceholder('Enter your API key')
|
||||
.setValue(this.plugin.settings.apiKey)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.apiKey = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
// 过滤器设置部分
|
||||
containerEl.createEl('h2', {text: 'Filter'});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Manage')
|
||||
.setDesc('选择要同步的文件夹')
|
||||
.addButton(button => button
|
||||
.setButtonText(this.getFolderFilterButtonText())
|
||||
.onClick(async () => {
|
||||
// 确保 API 已初始化
|
||||
if (!this.plugin.settings.apiKey) {
|
||||
new Notice('请先设置 API Key');
|
||||
return;
|
||||
}
|
||||
|
||||
// 打开文件夹选择模态框
|
||||
const modal = new FolderSelectModal(
|
||||
this.app,
|
||||
this.plugin.cuboxApi,
|
||||
this.plugin.settings.folderFilter,
|
||||
async (selectedFolders) => {
|
||||
this.plugin.settings.folderFilter = selectedFolders;
|
||||
await this.plugin.saveSettings();
|
||||
// 更新按钮文本
|
||||
button.setButtonText(this.getFolderFilterButtonText());
|
||||
}
|
||||
);
|
||||
modal.open();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Type Filter')
|
||||
.setDesc('Enter the xxxxx, 0 means manual sync')
|
||||
.addDropdown(dropdown => dropdown
|
||||
.addOption('', 'Sync all the items')
|
||||
.setValue(this.plugin.settings.typeFilter)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.typeFilter = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Status Filter')
|
||||
.setDesc('Only the items that match the status will be synced.')
|
||||
.addDropdown(dropdown => dropdown
|
||||
.addOption('all', 'Sync all the items')
|
||||
.addOption('read', 'Sync only read items')
|
||||
.addOption('starred', 'Sync only starred items')
|
||||
.addOption('annotated', 'Sync only annotated items')
|
||||
.setValue(this.plugin.settings.statusFilter || 'all')
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.statusFilter = value as 'all' | 'read' | 'starred' | 'annotated';
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
// 同步设置部分
|
||||
containerEl.createEl('h2', {text: 'Sync'});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Frequency')
|
||||
.setDesc('Enter the xxxxx, 0 means manual sync')
|
||||
.addText(text => text
|
||||
.setPlaceholder('Enter sync frequency in minutes')
|
||||
.setValue(String(this.plugin.settings.syncFrequency))
|
||||
.onChange(async (value) => {
|
||||
const numValue = parseInt(value);
|
||||
this.plugin.settings.syncFrequency = isNaN(numValue) ? 0 : numValue;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Target Folder')
|
||||
.setDesc('Select the folder you would like to sync')
|
||||
.addText(text => text
|
||||
.setPlaceholder('Enter target folder path')
|
||||
.setValue(this.plugin.settings.targetFolder)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.targetFolder = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('File Name Template')
|
||||
.setDesc('Enter the file name when the data is stored')
|
||||
.addText(text => text
|
||||
.setPlaceholder('Enter file name template')
|
||||
.setValue(this.plugin.settings.filenameTemplate)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.filenameTemplate = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Front Matter')
|
||||
.setDesc('Enter the metadata')
|
||||
.addTextArea(text => text
|
||||
.setPlaceholder('Enter front matter template')
|
||||
.setValue(this.plugin.settings.frontMatterTemplate)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.frontMatterTemplate = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Content Template')
|
||||
.setDesc('Enter the file name when the data is stored')
|
||||
.addTextArea(text => text
|
||||
.setPlaceholder('Enter content template')
|
||||
.setValue(this.plugin.settings.contentTemplate)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.contentTemplate = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Highlight Text in Content')
|
||||
.setDesc('Highlight text in articles')
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.highlightInContent)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.highlightInContent = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Date Format')
|
||||
.setDesc('Enter the xxxxx, 0 means manual sync')
|
||||
.addText(text => text
|
||||
.setPlaceholder('Enter date format')
|
||||
.setValue(this.plugin.settings.dateFormat)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.dateFormat = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
// 状态部分
|
||||
containerEl.createEl('h2', {text: 'Status'});
|
||||
containerEl.createEl('p', {text: `Last sync: ${this.plugin.formatLastSyncTime()}`});
|
||||
|
||||
// 添加测试连接按钮
|
||||
new Setting(containerEl)
|
||||
.setName('Test Connection')
|
||||
.setDesc('Test your Cubox API connection')
|
||||
.addButton(button => button
|
||||
.setButtonText('Test')
|
||||
.onClick(async () => {
|
||||
await this.plugin.cuboxApi.testConnection();
|
||||
}));
|
||||
|
||||
// 添加立即同步按钮
|
||||
new Setting(containerEl)
|
||||
.setName('Sync Now')
|
||||
.setDesc('Manually sync Cubox data')
|
||||
.addButton(button => button
|
||||
.setButtonText('Sync')
|
||||
.onClick(async () => {
|
||||
await this.plugin.syncCubox();
|
||||
}));
|
||||
}
|
||||
|
||||
// 获取文件夹过滤器按钮文本
|
||||
private getFolderFilterButtonText(): string {
|
||||
const count = this.plugin.settings.folderFilter.length;
|
||||
if (count === 0) {
|
||||
return 'Sync all folders';
|
||||
} else {
|
||||
return `已选择 ${count} 个文件夹`;
|
||||
}
|
||||
}
|
||||
}
|
||||
124
src/templateProcessor.ts
Normal file
124
src/templateProcessor.ts
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
import { CuboxArticle, CuboxHighlight } from './cuboxApi';
|
||||
import { formatISODateTime, generateSafeFileArticleName } from './utils';
|
||||
import * as Mustache from 'mustache';
|
||||
|
||||
export class TemplateProcessor {
|
||||
// 存储日期格式
|
||||
private dateFormat: string = 'yyyy-MM-dd';
|
||||
|
||||
/**
|
||||
* 设置日期格式
|
||||
* @param format 日期格式
|
||||
*/
|
||||
setDateFormat(format: string): void {
|
||||
this.dateFormat = format;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理文件名模板
|
||||
* @param template 模板字符串
|
||||
* @param article 文章数据
|
||||
*/
|
||||
processFilenameTemplate(template: string, article: CuboxArticle): string {
|
||||
if (!template) {
|
||||
return article.title || 'Untitled';
|
||||
}
|
||||
|
||||
// 准备用于 Mustache 的数据
|
||||
const data = this.prepareTemplateData(article);
|
||||
|
||||
// 使用 Mustache 渲染模板
|
||||
const filename = Mustache.render(template, data);
|
||||
|
||||
// 处理文件名安全性
|
||||
return generateSafeFileArticleName(filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理前置元数据模板
|
||||
* @param template 模板字符串
|
||||
* @param article 文章数据
|
||||
*/
|
||||
processFrontMatterTemplate(template: string, article: CuboxArticle): string {
|
||||
if (!template) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// 准备用于 Mustache 的数据
|
||||
const data = this.prepareTemplateData(article);
|
||||
|
||||
// 使用 Mustache 渲染模板
|
||||
return Mustache.render(template, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理内容模板
|
||||
* @param template 模板字符串
|
||||
* @param article 文章数据
|
||||
*/
|
||||
processContentTemplate(template: string, article: CuboxArticle): string {
|
||||
if (!template) {
|
||||
let content = ``;
|
||||
|
||||
if (article.content) {
|
||||
content += article.content + '\n\n';
|
||||
}
|
||||
|
||||
if (article.highlights && article.highlights.length > 0) {
|
||||
content += '## Highlights\n\n';
|
||||
article.highlights.forEach(highlight => {
|
||||
content += `- ${highlight.text}\n`;
|
||||
});
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
// 准备用于 Mustache 的数据
|
||||
const data = this.prepareTemplateData(article);
|
||||
|
||||
// 使用 Mustache 渲染模板
|
||||
return Mustache.render(template, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 准备用于 Mustache 模板的数据对象
|
||||
* @param article 文章数据
|
||||
*/
|
||||
private prepareTemplateData(article: CuboxArticle): any {
|
||||
// 格式化日期
|
||||
const createDate = article.createTime || '';
|
||||
const formattedDate = createDate ? formatISODateTime(createDate, this.dateFormat) : '';
|
||||
|
||||
// 格式化高亮内容
|
||||
let highlightsText = '';
|
||||
if (article.highlights && article.highlights.length > 0) {
|
||||
article.highlights.forEach(highlight => {
|
||||
highlightsText += `- ${highlight.text}\n`;
|
||||
});
|
||||
}
|
||||
|
||||
// 返回包含所有可用变量的对象
|
||||
return {
|
||||
cardTitle: article.title || 'Untitled',
|
||||
createDate: formattedDate,
|
||||
content: article.content || '',
|
||||
highlights: highlightsText,
|
||||
url: article.url || '',
|
||||
domain: article.domain || '',
|
||||
// 添加原始对象,以便可以访问所有属性
|
||||
article: article,
|
||||
// 添加高亮数组,以便可以在模板中循环
|
||||
highlightsList: article.highlights || []
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化日期 - 使用工具方法
|
||||
* @param dateStr 日期字符串
|
||||
* @param format 格式模板
|
||||
*/
|
||||
formatDate(dateStr: string, format: string): string {
|
||||
return formatISODateTime(dateStr, format);
|
||||
}
|
||||
}
|
||||
85
src/utils.ts
Normal file
85
src/utils.ts
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
// 文件名非法字符正则表达式
|
||||
export const ILLEGAL_CHAR_REGEX_FILE = /[<>:"/\\|?*\u0000-\u001F]/g;
|
||||
export const ILLEGAL_CHAR_REGEX_FOLDER = /[<>:"\\|?*\u0000-\u001F]/g;
|
||||
export const REPLACEMENT_CHAR = '-';
|
||||
|
||||
/**
|
||||
* 处理文件名中的非法字符
|
||||
* @param str 原始字符串
|
||||
* @returns 处理后的安全字符串
|
||||
*/
|
||||
export const replaceIllegalCharsFile = (str: string): string => {
|
||||
return str.replace(ILLEGAL_CHAR_REGEX_FILE, REPLACEMENT_CHAR);
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理文件夹名中的非法字符
|
||||
* @param str 原始字符串
|
||||
* @returns 处理后的安全字符串
|
||||
*/
|
||||
export const replaceIllegalCharsFolder = (str: string): string => {
|
||||
return str.replace(ILLEGAL_CHAR_REGEX_FOLDER, REPLACEMENT_CHAR);
|
||||
};
|
||||
|
||||
/**
|
||||
* 生成安全的文章文件名
|
||||
* @param title 文章标题
|
||||
* @returns 安全的文件名
|
||||
*/
|
||||
export const generateSafeFileArticleName = (title: string): string => {
|
||||
const safeTitle = replaceIllegalCharsFile(title).trim();
|
||||
return safeTitle;
|
||||
};
|
||||
|
||||
/**
|
||||
* 格式化 ISO 时间字符串为可读格式
|
||||
* @param isoString ISO 格式的时间字符串
|
||||
* @param format 可选的格式化模式
|
||||
* @returns 格式化后的时间字符串
|
||||
*/
|
||||
export const formatISODateTime = (isoString: string, format: string = 'yyyy-MM-dd HH:mm'): string => {
|
||||
if (!isoString) return '';
|
||||
|
||||
try {
|
||||
// 直接使用 Date 对象处理时间
|
||||
const date = new Date(isoString);
|
||||
|
||||
// 检查日期是否有效
|
||||
if (isNaN(date.getTime())) {
|
||||
return isoString;
|
||||
}
|
||||
|
||||
// 根据不同格式返回不同的日期字符串
|
||||
if (format === 'yyyy-MM-dd HH:mm' || format === 'YYYY-MM-dd HH:mm') {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}`;
|
||||
}
|
||||
else if (format === 'yyyy-MM-dd' || format === 'YYYY-MM-dd') {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
// 默认返回原始字符串
|
||||
return isoString;
|
||||
} catch (error) {
|
||||
console.error('Error formatting ISO date:', error);
|
||||
return isoString;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取当前时间的格式化字符串
|
||||
* @param format 格式化模式
|
||||
* @returns 格式化后的当前时间字符串
|
||||
*/
|
||||
export const getCurrentFormattedTime = (format: string = 'yyyy-MM-dd HH:mm'): string => {
|
||||
return formatISODateTime(new Date().toISOString(), format);
|
||||
};
|
||||
58
styles.css
58
styles.css
|
|
@ -6,3 +6,61 @@ available in the app when your plugin is enabled.
|
|||
If your plugin does not need CSS, delete this file.
|
||||
|
||||
*/
|
||||
|
||||
/* 文件夹选择模态框样式 */
|
||||
.folder-list {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
margin: 10px 0;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 4px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 20px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.modal-footer button {
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
background-color: var(--interactive-normal);
|
||||
color: var(--text-normal);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.modal-footer button.mod-cta {
|
||||
background-color: var(--interactive-accent);
|
||||
color: var(--text-on-accent);
|
||||
}
|
||||
|
||||
/* 更具体的选择器,针对设置面板中的文本区域 */
|
||||
.vertical-tab-content .setting-item:has(.setting-item-name:contains("Content Template")) textarea {
|
||||
min-height: 300px !important;
|
||||
height: 300px !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.vertical-tab-content .setting-item:has(.setting-item-name:contains("Front Matter")) textarea {
|
||||
min-height: 200px !important;
|
||||
height: 200px !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
/* 备用选择器,直接针对插件设置中的所有文本区域 */
|
||||
.plugin-settings-tab textarea {
|
||||
min-height: 150px;
|
||||
}
|
||||
|
||||
/* 针对特定插件的设置面板 */
|
||||
.plugin-settings-tab[data-type="obsidian-cubox-sync"] textarea {
|
||||
min-height: 150px;
|
||||
}
|
||||
|
||||
/* 最直接的方法:针对所有多行文本输入框 */
|
||||
.setting-item-control textarea {
|
||||
min-height: 200px !important;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue