organize directory/fix sync bug

This commit is contained in:
delphi 2025-03-14 09:17:14 +08:00
parent cc091591b6
commit 4d8c156f4f
11 changed files with 617 additions and 877 deletions

187
README.md
View file

@ -1,171 +1,48 @@
# Obsidian Cubox Sync
# Cubox Plugin for Obsidian
![Obsidian Cubox Sync](https://img.shields.io/badge/Obsidian-Cubox%20Sync-7963E6)
The official Cubox plugin for Obsidian allows you to synchronize articles and annotations from Cubox to Obsidian.
> 将 Cubox 中的文章和标注同步到 Obsidian打造无缝知识管理流程。
## Features
Obsidian Cubox Sync 是一个强大的 Obsidian 插件,它允许您将 Cubox 中收藏的文章、网页和标注内容自动同步到 Obsidian 笔记中。通过这个插件,您可以将 Cubox 作为信息收集工具,而 Obsidian 作为知识整理和思考工具,构建完整的个人知识管理系统。
- Automatic Sync: Periodically sync articles and annotations from Cubox to Obsidian
- Filtering: Filter content by folder, type, tags, and status
- Templates: Customize file names, front matter, and content format
- Annotation Sync: Sync highlighted content from Cubox to Obsidian notes
## ✨ 主要功能
## Installation
- **自动同步**:定时从 Cubox 同步文章和标注到 Obsidian
- **灵活过滤**:支持按文件夹、内容类型、状态和标签过滤同步内容
- **自定义模板**:使用 Mustache 模板语法自定义文件名、前置元数据和内容格式
- **高亮标注**:将 Cubox 中的高亮内容同步到 Obsidian 笔记中
- **增量同步**:智能识别新增和更新的内容,避免重复同步
- **手动触发**:支持通过命令或按钮手动触发同步
### Install from the Community
## 🚀 安装方法
1. Open Obsidian settings
2. Navigate to the "Community plugins" tab
3. Click the "Browse" button and search for "Cubox"
4. Click Install
### 从 Obsidian 社区插件库安装
### Manual Installation
1. 打开 Obsidian 设置
2. 进入 "第三方插件" 选项卡
3. 关闭 "安全模式"
4. 点击 "浏览" 按钮
5. 搜索 "Cubox Sync"
6. 点击安装
1. Download the latest `main.js`, `manifest.json`, and `styles.css` files
2. Create a `.obsidian/plugins/obsidian-cubox` folder in your Obsidian vault
3. Copy the downloaded files into this folder
4. Enable the plugin in Obsidian settings
### 手动安装
## Configuration
1. 下载最新版本的 `main.js`、`manifest.json` 和 `styles.css` 文件
2. 在您的 Obsidian 库中创建 `.obsidian/plugins/obsidian-cubox-sync` 文件夹
3. 将下载的文件复制到该文件夹中
4. 在 Obsidian 设置中启用插件
1. Cubox Server Domain: Select the Cubox server domain you use (cubox.cc or cubox.pro)
2. Cubox API Key: Enter your Cubox API key or link (generate it in the Cubox web settings under Extensions & Automation - API Extension)
## ⚙️ 配置说明
## Usage
### 连接设置
1. Before setting up, ensure you have selected the correct server and entered the API key
2. Only content that meets all filter conditions will be synced
3. Refer to the settings page for reference links to template variables
4. Each item is synced only once from Cubox, updates in Cubox will not be synced to Obsidian unless you change the target folder
5. It is recommended to set a longer sync interval or use manual sync to prevent syncing unfinished annotations
1. **Cubox 服务器域名**:选择您使用的 Cubox 服务器域名cubox.cc 或 cubox.pro
2. **Cubox API 密钥**:输入您的 Cubox API 密钥(可在 Cubox 网页版设置中创建)
## Dependencies
### 过滤设置
- [Mustache](https://mustache.github.io/): Template rendering
- [Luxon](https://moment.github.io/luxon/#/formatting?id=table-of-tokens): Date and time handling
- **文件夹过滤**:选择要同步的 Cubox 文件夹
- **类型过滤**:选择要同步的内容类型(文章、视频、图片等)
- **状态过滤**:选择要同步的内容状态(已读、未读等)
- **标签过滤**:选择要同步的标签
## License
### 同步设置
- **同步频率**:设置自动同步的时间间隔(分钟),设为 0 表示仅手动同步
- **目标文件夹**:设置同步内容保存到 Obsidian 的哪个文件夹
- **文件名模板**:设置生成的笔记文件名格式
- **前置元数据**:设置笔记的前置元数据字段
- **内容模板**:设置笔记内容的格式
- **日期格式**:设置日期时间的显示格式
## 🔍 模板变量
### 文件名模板变量
- `{{title}}` - 文章标题
- `{{article_title}}` - 原始文章标题
- `{{create_time}}` - 创建时间
- `{{update_time}}` - 更新时间
- `{{domain}}` - 域名
- `{{type}}` - 内容类型
### 前置元数据变量
- `title` - 文章标题
- `article_title` - 原始文章标题
- `tags` - 标签列表
- `create_time` - 创建时间
- `update_time` - 更新时间
- `domain` - 域名
- `url` - 原始链接
- `cubox_url` - Cubox 链接
- `description` - 描述
- `words_count` - 字数统计
- `type` - 内容类型
- `id` - 文章 ID
### 内容模板变量
- `{{title}}` - 文章标题
- `{{description}}` - 描述
- `{{content}}` - 文章内容
- `{{content_highlighted}}` - 带高亮的文章内容
- `{{highlights.length}}` - 高亮数量
- `{{#highlights}}` ... `{{/highlights}}` - 高亮内容循环
- `{{text}}` - 高亮文本
- `{{note}}` - 高亮笔记
- `{{cubox_url}}` - 高亮链接
- `{{color}}` - 高亮颜色
- `{{create_time}}` - 高亮创建时间
## 📝 使用示例
### 基本内容模板示例
```
# {{{title}}}
{{{description}}}
[Read in Cubox]({{{cubox_url}}})
[Read Original]({{{url}}})
{{{content}}}
{{#highlights.length}}
## Annotations
{{#highlights}}
> {{{text}}}
{{{note}}}
[Link]({{{cubox_url}}})
{{/highlights}}
{{/highlights.length}}
```
### 前置元数据示例
```
title,url,tags,create_time,domain
```
或使用别名:
```
title,url,tags,create_time::date,domain::source
```
## 🔄 同步流程
1. 插件会根据设置的频率自动同步,或者您可以手动触发同步
2. 同步时会根据过滤条件获取 Cubox 中的文章
3. 对于每篇文章,插件会根据模板生成文件名、前置元数据和内容
4. 生成的笔记会保存到指定的目标文件夹中
5. 同步完成后,状态栏会显示最后同步时间
## 🛠️ 开发者信息
### 构建项目
1. 克隆仓库
2. 安装依赖:`npm install`
3. 开发模式:`npm run dev`
4. 构建生产版本:`npm run build`
### 主要依赖
- Mustache用于模板渲染
- Luxon用于日期时间处理
## 📄 许可证
本项目采用 MIT 许可证。详情请参阅 [LICENSE](LICENSE) 文件。
## 🙏 致谢
- [Obsidian](https://obsidian.md/) - 知识管理工具
- [Cubox](https://cubox.pro/) - 网络内容收藏工具
- 所有贡献者和用户
---
如有问题或建议,请在 GitHub 仓库中提交 Issue。
This project is licensed under the [MIT License](https://choosealicense.com/licenses/mit/).

View file

@ -125,7 +125,6 @@ export class CuboxApi {
} = { lastCardId: null, lastCardUpdateTime: null }
): Promise<{ articles: CuboxArticle[], hasMore: boolean}> {
try {
// 创建请求体对象而不是URL参数
const requestBody: Record<string, any> = {
limit: 50
};
@ -203,7 +202,6 @@ export class CuboxApi {
return response.data;
} catch (error) {
console.error(`获取文章 ${articleId} 详情失败:`, error);
return null;
}
}
@ -219,7 +217,6 @@ export class CuboxApi {
return response.data ?? [];
} catch (error) {
console.error('获取 Cubox 文件夹列表失败:', error);
new Notice('Failed to get Cubox folders');
throw error;
}
}
@ -235,7 +232,6 @@ export class CuboxApi {
return response.data ?? [];
} catch (error) {
console.error('获取 Cubox 标签列表失败:', error);
new Notice('Failed to get Cubox tags');
throw error;
}
}

563
src/cuboxSetting.ts Normal file
View file

@ -0,0 +1,563 @@
import { App, Notice, PluginSettingTab, Setting } from 'obsidian';
import type CuboxSyncPlugin from './main';
import { FRONT_MATTER_VARIABLES } from './templateProcessor';
import { ALL_FOLDERS_ID, FolderSelectModal } from './modal/folderSelectModal';
import { filenameTemplateInstructions, metadataVariablesInstructions, contentTemplateInstructions, cuboxDateFormat } from './templateInstructions';
import { ALL_CONTENT_TYPES, TypeSelectModal } from './modal/typeSelectModal';
import { ALL_STATUS_ID, StatusSelectModal } from './modal/statusSelectModal';
import { ALL_ITEMS, TagSelectModal } from './modal/tagSelectModal';
export const DEFAULT_CONTENT_TEMPLATE = `# {{{title}}}
{{{description}}}
[Read in Cubox]({{{cubox_url}}})
[Read Original]({{{url}}})
---
{{#highlights.length}}
## Annotations
{{#highlights}}
> {{{text}}}
{{#note}}
{{{note}}}
{{/note}}
[Link]({{{cubox_url}}})
{{/highlights}}
{{/highlights.length}}`
export interface CuboxSyncSettings {
domain: string;
apiKey: string;
folderFilter: string[];
typeFilter: string[];
statusFilter: string[];
isRead: boolean;
isStarred: boolean;
isAnnotated: boolean;
tagsFilter: string[];
syncFrequency: number;
targetFolder: string;
filenameTemplate: string;
frontMatterVariables: string[];
contentTemplate: string;
highlightInContent: boolean;
dateFormat: string;
lastSyncTime: number;
lastSyncCardId: string | null;
lastCardUpdateTime: string | null;
syncing: boolean;
}
export const DEFAULT_SETTINGS: CuboxSyncSettings = {
domain: '',
apiKey: '',
folderFilter: [ALL_FOLDERS_ID],
typeFilter: ALL_CONTENT_TYPES,
statusFilter: [ALL_STATUS_ID],
isRead: true,
isStarred: true,
isAnnotated: true,
tagsFilter: [ALL_ITEMS],
syncFrequency: 30, // 分钟
targetFolder: 'Cubox',
filenameTemplate: '{{title}}-{{create_time}}',
frontMatterVariables: ['id', 'cubox_url', 'url', 'tags'],
contentTemplate: DEFAULT_CONTENT_TEMPLATE,
highlightInContent: true,
dateFormat: 'yyyy-MM-dd',
lastSyncTime: 0,
lastSyncCardId: null,
lastCardUpdateTime: null,
syncing: false,
}
export class CuboxSyncSettingTab extends PluginSettingTab {
plugin: CuboxSyncPlugin;
apiKeySetting: Setting;
constructor(app: App, plugin: CuboxSyncPlugin) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const {containerEl} = this;
containerEl.empty();
// 添加 Cubox 标题和版本信息
new Setting(containerEl)
.setName('Cubox')
.setDesc(`Created by Cubox, version ${this.plugin.manifest.version}`)
// 连接设置部分
containerEl.createEl('h3', {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('', 'Choose Region')
.addOption('cubox.cc', 'cubox.cc (international)')
.addOption('cubox.pro', 'cubox.pro')
.setValue(this.plugin.settings.domain)
.onChange(async (value) => {
this.plugin.settings.domain = value;
await this.plugin.saveSettings();
// 更新 API Key 设置的描述和状态
this.updateApiKeySetting();
// 更新 Cubox API 配置
this.plugin.updateCuboxApiConfig(value, this.plugin.settings.apiKey);
}));
// 添加 API Key 设置
this.apiKeySetting = new Setting(containerEl)
.setName('Your Cubox API Key')
.setDesc('Please select a region first')
.addText(text => {
text.setPlaceholder('Enter your API key')
.setValue(this.plugin.settings.apiKey)
.onChange(async (value) => {
// 处理用户可能粘贴了完整URL的情况
let apiKey = value.trim();
// 检查是否是URL格式
if (apiKey.includes('://')) {
try {
// 尝试解析URL或路径
const url = apiKey.includes('://') ? new URL(apiKey) : null;
// 如果是URL获取路径的最后一部分
if (url) {
const pathParts = url.pathname.split('/').filter(part => part.length > 0);
if (pathParts.length > 0) {
apiKey = pathParts[pathParts.length - 1];
}
}
// 更新输入框显示提取的API key
text.setValue(apiKey);
} catch (e) {
new Notice('Invalid API key');
}
}
this.plugin.settings.apiKey = apiKey;
await this.plugin.saveSettings();
// 更新 Cubox API 配置
this.plugin.updateCuboxApiConfig(this.plugin.settings.domain, apiKey);
});
// 如果未选择域名,则禁用输入框
if (!this.plugin.settings.domain) {
text.inputEl.disabled = true;
}
return text;
});
// 初始化 API Key 设置的描述和状态
this.updateApiKeySetting();
// 过滤器设置部分
containerEl.createEl('h3', {text: 'Filter'});
new Setting(containerEl)
.setName('Folder Filter')
.setDesc('Manage Cubox folders to be synced')
.addButton(button => button
.setButtonText(this.getFolderFilterButtonText())
.setCta()
.onClick(async () => {
if (!this.plugin.settings.apiKey) {
new Notice('Please enter the API key first');
return;
}
// 显示加载状态
button.setButtonText('Loading folders...');
try {
// 先获取文件夹数据
const folders = await this.plugin.cuboxApi.getFolders();
// 打开文件夹选择模态框,传入已获取的文件夹数据
const modal = new FolderSelectModal(
this.app,
folders, // 直接传入获取到的文件夹数据
this.plugin.settings.folderFilter,
async (selectedFolders) => {
this.plugin.settings.folderFilter = selectedFolders;
await this.plugin.saveSettings();
// 更新按钮文本
button.setButtonText(this.getFolderFilterButtonText());
}
);
modal.open();
} catch (error) {
console.error('获取文件夹列表失败:', error);
new Notice('Failed to get Cubox folders');
// 恢复按钮文本
button.setButtonText(this.getFolderFilterButtonText());
}
button.setButtonText(this.getFolderFilterButtonText());
}));
new Setting(containerEl)
.setName('Tag Filter')
.setDesc('Manage Cubox tags to be synced')
.addButton(button => button
.setButtonText(this.getTagFilterButtonText())
.setCta()
.onClick(async () => {
if (!this.plugin.settings.apiKey) {
new Notice('Please enter the API key first');
return;
}
// 显示加载状态
button.setButtonText('Loading tags...');
try {
// 先获取标签数据
const tags = await this.plugin.cuboxApi.getTags();
// 打开标签选择模态框,传入已获取的标签数据
const modal = new TagSelectModal(
this.app,
tags, // 直接传入获取到的标签数据
this.plugin.settings.tagsFilter,
async (selectedTags) => {
this.plugin.settings.tagsFilter = selectedTags;
await this.plugin.saveSettings();
// 更新按钮文本
button.setButtonText(this.getTagFilterButtonText());
}
);
modal.open();
} catch (error) {
console.error('获取标签列表失败:', error);
new Notice('Failed to get Cubox tags');
// 恢复按钮文本
button.setButtonText(this.getTagFilterButtonText());
}
button.setButtonText(this.getTagFilterButtonText());
}));
new Setting(containerEl)
.setName('Type Filter')
.setDesc('Manage Cubox content types to be synced')
.addButton(button => button
.setButtonText(this.getTypeFilterButtonText())
.setCta()
.onClick(async () => {
if (!this.plugin.settings.apiKey) {
new Notice('Please enter the API key first');
return;
}
// 打开类型选择模态框
const modal = new TypeSelectModal(
this.app,
this.plugin.settings.typeFilter,
async (selectedTypes) => {
this.plugin.settings.typeFilter = selectedTypes;
await this.plugin.saveSettings();
// 更新按钮文本
button.setButtonText(this.getTypeFilterButtonText());
}
);
modal.open();
}));
new Setting(containerEl)
.setName('Status Filter')
.setDesc('Manage Cubox content status to be synced')
.addButton(button => button
.setButtonText(this.getStatusFilterButtonText())
.setCta()
.onClick(async () => {
if (!this.plugin.settings.apiKey) {
new Notice('Please enter the API key first');
return;
}
// 创建状态值对象
const statusValues = {
read: this.plugin.settings.isRead,
starred: this.plugin.settings.isStarred,
annotated: this.plugin.settings.isAnnotated
};
// 打开状态选择模态框
const modal = new StatusSelectModal(
this.app,
this.plugin.settings.statusFilter,
statusValues,
async (selectedStatuses, newStatusValues) => {
this.plugin.settings.statusFilter = selectedStatuses;
// 更新状态布尔值
this.plugin.settings.isRead = newStatusValues.read;
this.plugin.settings.isStarred = newStatusValues.starred;
this.plugin.settings.isAnnotated = newStatusValues.annotated;
await this.plugin.saveSettings();
// 更新按钮文本
button.setButtonText(this.getStatusFilterButtonText());
}
);
modal.open();
}));
// 同步设置部分
containerEl.createEl('h3', {text: 'Sync'});
new Setting(containerEl)
.setName('Sync Interval')
.setDesc('Auto sync interval (in minutes). 0 means manual sync. Each item syncs only once. Subsequent updates won\'t be synced, and modifications in Obsidian won\'t affect Cubox. We recommend avoiding frequent updates.')
.addText(text => {
const textField = text
.setPlaceholder('Enter the interval')
.setValue(String(this.plugin.settings.syncFrequency))
.onChange(async (value) => {
let frequency = parseInt(value)
if (isNaN(frequency)) {
new Notice('Frequency must be a positive integer')
return
}
frequency = Math.min(frequency, 9999); // 最大24小时
this.plugin.settings.syncFrequency = frequency;
await this.plugin.saveSettings();
this.plugin.setupAutoSync();
// 将处理后的值回填到输入框
text.setValue(String(frequency));
});
return textField;
});
new Setting(containerEl)
.setName('Folder')
.setDesc('Select the folder you\'d like to sync to')
.addText(text => text
.setPlaceholder('Enter target folder path')
.setValue(this.plugin.settings.targetFolder)
.onChange(async (value) => {
this.plugin.settings.lastSyncCardId = null;
this.plugin.settings.targetFolder = value;
await this.plugin.saveSettings();
}));
// 更新文件名模板设置
const filenameInstructionsFragment = document.createRange().createContextualFragment(filenameTemplateInstructions);
new Setting(containerEl)
.setName('File Name Template')
.setDesc(filenameInstructionsFragment)
.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();
}))
.addButton(button => button
.setIcon('reset')
.setTooltip('Reset to default')
.onClick(async () => {
this.plugin.settings.filenameTemplate = DEFAULT_SETTINGS.filenameTemplate;
await this.plugin.saveSettings();
// 刷新显示
const textComponent = button.buttonEl.parentElement?.parentElement?.querySelector('input');
if (textComponent) {
textComponent.value = DEFAULT_SETTINGS.filenameTemplate;
}
}));
// 更新前置元数据模板设置
const metadataInstructionsFragment = document.createRange().createContextualFragment(metadataVariablesInstructions);
new Setting(containerEl)
.setName('Metadata Variables')
.setDesc(metadataInstructionsFragment)
.addTextArea(text => text
.setPlaceholder('Enter front matter variables')
.setValue(this.plugin.settings.frontMatterVariables.join(','))
.onChange(async (value) => {
this.plugin.settings.frontMatterVariables = value
.split(',')
.map((v) => v.trim())
.filter(
(v, i, a) =>
FRONT_MATTER_VARIABLES.includes(v.split('::')[0]) &&
a.indexOf(v) === i,
)
await this.plugin.saveSettings();
})
// 设置文本区域的大小
.then(textArea => {
textArea.inputEl.rows = 20;
textArea.inputEl.cols = 30;
}))
.addButton(button => button
.setIcon('reset')
.setTooltip('Reset to default')
.onClick(async () => {
this.plugin.settings.frontMatterVariables = DEFAULT_SETTINGS.frontMatterVariables;
await this.plugin.saveSettings();
// 刷新显示
const textArea = button.buttonEl.parentElement?.parentElement?.querySelector('textarea');
if (textArea) {
textArea.value = DEFAULT_SETTINGS.frontMatterVariables.join(',');
}
}));
// 更新内容模板设置
const contentInstructionsFragment = document.createRange().createContextualFragment(contentTemplateInstructions);
new Setting(containerEl)
.setName('Content Template')
.setDesc(contentInstructionsFragment)
.addTextArea(text => text
.setPlaceholder('Enter content template')
.setValue(this.plugin.settings.contentTemplate)
.onChange(async (value) => {
this.plugin.settings.contentTemplate = value;
await this.plugin.saveSettings();
})
// 设置文本区域的大小
.then(textArea => {
textArea.inputEl.rows = 20;
textArea.inputEl.cols = 30;
}))
.addButton(button => button
.setIcon('reset')
.setTooltip('Reset to default')
.onClick(async () => {
this.plugin.settings.contentTemplate = DEFAULT_SETTINGS.contentTemplate;
await this.plugin.saveSettings();
// 刷新显示
const textArea = button.buttonEl.parentElement?.parentElement?.querySelector('textarea');
if (textArea) {
textArea.value = DEFAULT_SETTINGS.contentTemplate;
}
}));
// 更新日期格式模板设置
const dateFormatInstructionsFragment = document.createRange().createContextualFragment(cuboxDateFormat);
new Setting(containerEl)
.setName('Date Format')
.setDesc(dateFormatInstructionsFragment)
.addText(text => text
.setPlaceholder('Enter date format')
.setValue(this.plugin.settings.dateFormat)
.onChange(async (value) => {
this.plugin.settings.dateFormat = value;
await this.plugin.saveSettings();
this.plugin.updateTemplateProcessorDateFormat(value);
}));
// 状态部分
containerEl.createEl('h3', {text: 'Status'});
containerEl.createEl('p', {text: `Last sync: ${this.plugin.formatLastSyncTime()}`});
}
// 更新 API Key 设置的描述和状态
private updateApiKeySetting(): void {
const domain = this.plugin.settings.domain;
const textComponent = this.apiKeySetting.components[0] as any;
if (!domain) {
// 未选择域名
this.apiKeySetting.setDesc('Please select a region first');
textComponent.inputEl.disabled = true;
} else {
// 已选择域名,更新描述和链接
const url = `https://${domain}/my/settings/extensions`;
const descEl = this.apiKeySetting.descEl;
// 清空现有描述
descEl.empty();
// 添加新的描述文本和链接
descEl.appendChild(
createSpan({text: 'You can create a key in the '})
);
descEl.appendChild(
createEl('a', {
text: 'Extension Settings',
href: url,
attr: {
target: '_blank',
rel: 'noopener'
}
})
);
descEl.appendChild(
createSpan({text: ' of Cubox web app.'})
);
textComponent.inputEl.disabled = false;
}
}
private getFolderFilterButtonText(): string {
const folderFilters = this.plugin.settings.folderFilter;
if (!folderFilters || folderFilters.length === 0) {
return 'Manage';
} else if (folderFilters.includes(ALL_FOLDERS_ID)) {
return 'All Folders';
} else {
return `${folderFilters.length} selected`;
}
}
private getTypeFilterButtonText(): string {
const typeFilters = this.plugin.settings.typeFilter;
if (!typeFilters || typeFilters.length === 0) {
return 'Manage';
} else if (typeFilters.length === ALL_CONTENT_TYPES.length) {
return 'All Types';
} else {
return `${typeFilters.length} selected`;
}
}
private getStatusFilterButtonText(): string {
const statusFilters = this.plugin.settings.statusFilter;
if (!statusFilters || statusFilters.length === 0) {
return 'Manage';
} else if (statusFilters.includes('all')) {
return 'All Items';
} else {
return `${statusFilters.length} selected`;
}
}
private getTagFilterButtonText(): string {
const tagFilters = this.plugin.settings.tagsFilter;
if (!tagFilters || tagFilters.length === 0) {
return 'Manage';
} else if (tagFilters.includes(ALL_ITEMS)) {
return 'All Items';
} else if (tagFilters.includes('')) {
return 'No Tags';
} else {
return `${tagFilters.length} selected`;
}
}
}

View file

@ -1,80 +1,8 @@
import { addIcon, App, Notice, Plugin, PluginSettingTab, Setting, TFile } from 'obsidian';
import { addIcon, Notice, Plugin, TFile } from 'obsidian';
import { CuboxApi } from './cuboxApi';
import { TemplateProcessor, FRONT_MATTER_VARIABLES } from './templateProcessor';
import { TemplateProcessor } from './templateProcessor';
import { formatDateTime } from './utils';
import { ALL_FOLDERS_ID, FolderSelectModal } from './modal/folderSelectModal';
import { filenameTemplateInstructions, metadataVariablesInstructions, contentTemplateInstructions, cuboxDateFormat } from './templateInstructions';
import { ALL_CONTENT_TYPES, TypeSelectModal } from './modal/typeSelectModal';
import { StatusSelectModal } from './modal/statusSelectModal';
import { ALL_ITEMS, TagSelectModal } from './modal/tagSelectModal';
import { ALL_STATUS_ID } from './modal/statusSelectModal';
export const DEFAULT_CONTENT_TEMPLATE = `# {{{title}}}
{{{description}}}
[Read in Cubox]({{{cubox_url}}})
[Read Original]({{{url}}})
{{{content}}}
{{#highlights.length}}
## Annotations
{{#highlights}}
> {{{text}}}
{{{note}}}
[Link]({{{cubox_url}}})
{{/highlights}}
{{/highlights.length}}`
interface CuboxSyncSettings {
domain: string; // 可以是 'cubox.cc' | 'cubox.pro' | ''
apiKey: string;
folderFilter: string[];
typeFilter: string[];
statusFilter: string[];
isRead: boolean;
isStarred: boolean;
isAnnotated: boolean;
tagsFilter: string[];
syncFrequency: number;
targetFolder: string;
filenameTemplate: string;
frontMatterVariables: string[];
contentTemplate: string;
highlightInContent: boolean;
dateFormat: string;
lastSyncTime: number;
lastSyncCardId: string | null;
lastCardUpdateTime: string | null;
syncing: boolean;
}
const DEFAULT_SETTINGS: CuboxSyncSettings = {
domain: '',
apiKey: '',
folderFilter: [ALL_FOLDERS_ID],
typeFilter: ALL_CONTENT_TYPES,
statusFilter: [ALL_STATUS_ID],
isRead: true,
isStarred: true,
isAnnotated: true,
tagsFilter: [ALL_ITEMS],
syncFrequency: 30, // 分钟
targetFolder: 'Cubox',
filenameTemplate: '{{title}}-{{create_time}}',
frontMatterVariables: ['id', 'cubox_url', 'url', 'tags'],
contentTemplate: DEFAULT_CONTENT_TEMPLATE,
highlightInContent: true,
dateFormat: 'yyyy-MM-dd',
lastSyncTime: 0,
lastSyncCardId: null,
lastCardUpdateTime: null,
syncing: false,
}
import { CuboxSyncSettingTab, CuboxSyncSettings, DEFAULT_SETTINGS } from './cuboxSetting';
export default class CuboxSyncPlugin extends Plugin {
@ -90,10 +18,8 @@ export default class CuboxSyncPlugin extends Plugin {
// 初始化 API 和模板处理器
this.cuboxApi = new CuboxApi(this.settings.domain, this.settings.apiKey);
this.templateProcessor = new TemplateProcessor();
// 设置模板处理器的日期格式
this.templateProcessor.setDateFormat(this.settings.dateFormat);
// 添加左侧图标
const iconId = 'Cubox'
addIcon(iconId, '<svg viewBox="0 0 19 19" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#clip0_832_147)"><path d="M16.4832 14.9619C16.3985 19.9047 13.3129 19.7653 9.40222 19.7653C5.49152 19.7653 3.46973 18.9255 3.46973 14.6576C3.46973 10.2101 5.49152 6.90909 9.40222 6.90909C13.3129 6.90909 16.4832 10.5144 16.4832 14.9619Z" stroke="currentColor" stroke-width="1.72727"/><rect x="0.863636" y="0.863636" width="17.2727" height="17.2727" rx="8.63636" stroke="currentColor" stroke-width="1.72727"/></g><ellipse cx="6.90905" cy="12.2205" rx="0.863636" ry="0.993182" fill="currentColor"/><ellipse cx="9.49987" cy="12.2205" rx="0.863636" ry="0.993182" fill="currentColor"/><defs><clipPath id="clip0_832_147"><rect width="19" height="19" rx="9.5" fill="white"/></clipPath></defs></svg>');
@ -111,7 +37,7 @@ export default class CuboxSyncPlugin extends Plugin {
// 添加同步命令
this.addCommand({
id: 'sync-cubox-data',
name: '同步 Cubox 数据',
name: 'Sync Cubox Data',
callback: async () => {
await this.syncCubox();
}
@ -160,7 +86,6 @@ export default class CuboxSyncPlugin extends Plugin {
}
async syncCubox() {
// 如果已经在同步中,则跳过
if (this.settings.syncing) {
new Notice('Sync is in progress, please wait.');
return;
@ -171,22 +96,19 @@ export default class CuboxSyncPlugin extends Plugin {
this.settings.syncing = true;
await this.saveSettings();
// 更新状态栏显示
this.statusBarItem.setText(`正在同步 Cubox...`);
// 确保目标文件夹存在
await this.ensureTargetFolder();
// 如果选择继续上次同步,则使用保存的 lastSyncCardId
let lastCardId: string | null = this.settings.lastSyncCardId;
let lastCardUpdateTime: string | null = this.settings.lastCardUpdateTime;
let hasMore = true;
let syncCount = 0;
let errorCount = 0;
let skipCount = 0;
// 分页获取所有文章
while (hasMore) {
// 获取文章列表,传递状态布尔值
const result = await this.cuboxApi.getArticles(
{
lastCardId: lastCardId,
@ -276,8 +198,8 @@ export default class CuboxSyncPlugin extends Plugin {
hasMore = moreArticles;
if (articles.length > 0) {
const lastCardId = articles[articles.length - 1].id;
const lastCardUpdateTime = articles[articles.length - 1].update_time;
lastCardId = articles[articles.length - 1].id;
lastCardUpdateTime = articles[articles.length - 1].update_time;
this.settings.lastSyncCardId = lastCardId;
this.settings.lastCardUpdateTime = lastCardUpdateTime;
@ -306,10 +228,7 @@ export default class CuboxSyncPlugin extends Plugin {
async ensureTargetFolder() {
const folderPath = this.settings.targetFolder;
// 检查文件夹是否存在
if (!(await this.app.vault.adapter.exists(folderPath))) {
// 创建文件夹
await this.app.vault.createFolder(folderPath);
}
}
@ -319,7 +238,6 @@ export default class CuboxSyncPlugin extends Plugin {
return 'Never';
}
// 使用新的格式化方法
return formatDateTime(new Date(this.settings.lastSyncTime).toISOString(), 'yyyy-MM-dd HH:mm');
}
@ -332,504 +250,4 @@ export default class CuboxSyncPlugin extends Plugin {
}
}
class CuboxSyncSettingTab extends PluginSettingTab {
plugin: CuboxSyncPlugin;
apiKeySetting: Setting; // 添加一个属性来存储 API Key 设置
constructor(app: App, plugin: CuboxSyncPlugin) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const {containerEl} = this;
containerEl.empty();
// 添加 Cubox 标题和版本信息
new Setting(containerEl)
.setName('Cubox')
.setDesc(`Created by Cubox, version ${this.plugin.manifest.version}`)
//.setClass('cubox-header-setting');
// 连接设置部分
containerEl.createEl('h3', {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('', 'Choose Region')
.addOption('cubox.cc', 'cubox.cc (international)')
.addOption('cubox.pro', 'cubox.pro')
.setValue(this.plugin.settings.domain)
.onChange(async (value) => {
this.plugin.settings.domain = value;
await this.plugin.saveSettings();
// 更新 API Key 设置的描述和状态
this.updateApiKeySetting();
// 更新 Cubox API 配置
this.plugin.updateCuboxApiConfig(value, this.plugin.settings.apiKey);
}));
// 添加 API Key 设置
this.apiKeySetting = new Setting(containerEl)
.setName('Your Cubox API Key')
.setDesc('Please select a region first')
.addText(text => {
text.setPlaceholder('Enter your API key')
.setValue(this.plugin.settings.apiKey)
.onChange(async (value) => {
// 处理用户可能粘贴了完整URL的情况
let apiKey = value.trim();
// 检查是否是URL格式
if (apiKey.includes('://')) {
try {
// 尝试解析URL或路径
const url = apiKey.includes('://') ? new URL(apiKey) : null;
// 如果是URL获取路径的最后一部分
if (url) {
const pathParts = url.pathname.split('/').filter(part => part.length > 0);
if (pathParts.length > 0) {
apiKey = pathParts[pathParts.length - 1];
}
}
// 更新输入框显示提取的API key
text.setValue(apiKey);
} catch (e) {
new Notice('Invalid API key');
}
}
this.plugin.settings.apiKey = apiKey;
await this.plugin.saveSettings();
// 更新 Cubox API 配置
this.plugin.updateCuboxApiConfig(this.plugin.settings.domain, apiKey);
});
// 如果未选择域名,则禁用输入框
if (!this.plugin.settings.domain) {
text.inputEl.disabled = true;
}
return text;
});
// 初始化 API Key 设置的描述和状态
this.updateApiKeySetting();
// 过滤器设置部分
containerEl.createEl('h3', {text: 'Filter'});
new Setting(containerEl)
.setName('Folder Filter')
.setDesc('Manage Cubox folders to be synced')
.addButton(button => button
.setButtonText(this.getFolderFilterButtonText())
.setCta()
.onClick(async () => {
// 确保 API 已初始化
if (!this.plugin.settings.apiKey) {
new Notice('Please enter the API key first');
return;
}
// 显示加载状态
button.setButtonText('Loading folders...');
try {
// 先获取文件夹数据
const folders = await this.plugin.cuboxApi.getFolders();
// 打开文件夹选择模态框,传入已获取的文件夹数据
const modal = new FolderSelectModal(
this.app,
folders, // 直接传入获取到的文件夹数据
this.plugin.settings.folderFilter,
async (selectedFolders) => {
this.plugin.settings.folderFilter = selectedFolders;
await this.plugin.saveSettings();
// 更新按钮文本
button.setButtonText(this.getFolderFilterButtonText());
}
);
modal.open();
} catch (error) {
console.error('获取文件夹列表失败:', error);
new Notice('Failed to get Cubox folders');
// 恢复按钮文本
button.setButtonText(this.getFolderFilterButtonText());
}
button.setButtonText(this.getFolderFilterButtonText());
}));
new Setting(containerEl)
.setName('Tag Filter')
.setDesc('Manage Cubox tags to be synced')
.addButton(button => button
.setButtonText(this.getTagFilterButtonText())
.setCta()
.onClick(async () => {
// 确保 API 已初始化
if (!this.plugin.settings.apiKey) {
new Notice('Please enter the API key first');
return;
}
// 显示加载状态
button.setButtonText('Loading tags...');
try {
// 先获取标签数据
const tags = await this.plugin.cuboxApi.getTags();
// 打开标签选择模态框,传入已获取的标签数据
const modal = new TagSelectModal(
this.app,
tags, // 直接传入获取到的标签数据
this.plugin.settings.tagsFilter,
async (selectedTags) => {
this.plugin.settings.tagsFilter = selectedTags;
await this.plugin.saveSettings();
// 更新按钮文本
button.setButtonText(this.getTagFilterButtonText());
}
);
modal.open();
} catch (error) {
console.error('获取标签列表失败:', error);
new Notice('Failed to get Cubox tags');
// 恢复按钮文本
button.setButtonText(this.getTagFilterButtonText());
}
button.setButtonText(this.getTagFilterButtonText());
}));
new Setting(containerEl)
.setName('Type Filter')
.setDesc('Manage Cubox content types to be synced')
.addButton(button => button
.setButtonText(this.getTypeFilterButtonText())
.setCta()
.onClick(async () => {
// 确保 API 已初始化
if (!this.plugin.settings.apiKey) {
new Notice('Please enter the API key first');
return;
}
// 打开类型选择模态框
const modal = new TypeSelectModal(
this.app,
this.plugin.settings.typeFilter,
async (selectedTypes) => {
this.plugin.settings.typeFilter = selectedTypes;
await this.plugin.saveSettings();
// 更新按钮文本
button.setButtonText(this.getTypeFilterButtonText());
}
);
modal.open();
}));
new Setting(containerEl)
.setName('Status Filter')
.setDesc('Manage Cubox content status to be synced')
.addButton(button => button
.setButtonText(this.getStatusFilterButtonText())
.setCta()
.onClick(async () => {
// 确保 API 已初始化
if (!this.plugin.settings.apiKey) {
new Notice('Please enter the API key first');
return;
}
// 创建状态值对象
const statusValues = {
read: this.plugin.settings.isRead,
starred: this.plugin.settings.isStarred,
annotated: this.plugin.settings.isAnnotated
};
// 打开状态选择模态框
const modal = new StatusSelectModal(
this.app,
this.plugin.settings.statusFilter,
statusValues,
async (selectedStatuses, newStatusValues) => {
this.plugin.settings.statusFilter = selectedStatuses;
// 更新状态布尔值
this.plugin.settings.isRead = newStatusValues.read;
this.plugin.settings.isStarred = newStatusValues.starred;
this.plugin.settings.isAnnotated = newStatusValues.annotated;
await this.plugin.saveSettings();
// 更新按钮文本
button.setButtonText(this.getStatusFilterButtonText());
}
);
modal.open();
}));
// 同步设置部分
containerEl.createEl('h3', {text: 'Sync'});
new Setting(containerEl)
.setName('Sync Interval')
.setDesc('Auto sync interval (in minutes). 0 means manual sync. Each item syncs only once. Subsequent updates won\'t be synced, and modifications in Obsidian won\'t affect Cubox. We recommend avoiding frequent updates.')
.addText(text => {
const textField = text
.setPlaceholder('Enter the interval')
.setValue(String(this.plugin.settings.syncFrequency))
.onChange(async (value) => {
let frequency = parseInt(value)
if (isNaN(frequency)) {
new Notice('Frequency must be a positive integer')
return
}
frequency = Math.min(frequency, 9999); // 最大24小时
this.plugin.settings.syncFrequency = frequency;
await this.plugin.saveSettings();
this.plugin.setupAutoSync();
// 将处理后的值回填到输入框
text.setValue(String(frequency));
});
return textField;
});
new Setting(containerEl)
.setName('Folder')
.setDesc('Select the folder you\'d like to sync to')
.addText(text => text
.setPlaceholder('Enter target folder path')
.setValue(this.plugin.settings.targetFolder)
.onChange(async (value) => {
this.plugin.settings.lastSyncCardId = null;
this.plugin.settings.targetFolder = value;
await this.plugin.saveSettings();
}));
// 更新文件名模板设置
const filenameInstructionsFragment = document.createRange().createContextualFragment(filenameTemplateInstructions);
new Setting(containerEl)
.setName('File Name Template')
.setDesc(filenameInstructionsFragment)
.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();
}))
.addButton(button => button
.setIcon('reset')
.setTooltip('Reset to default')
.onClick(async () => {
this.plugin.settings.filenameTemplate = DEFAULT_SETTINGS.filenameTemplate;
await this.plugin.saveSettings();
// 刷新显示
const textComponent = button.buttonEl.parentElement?.parentElement?.querySelector('input');
if (textComponent) {
textComponent.value = DEFAULT_SETTINGS.filenameTemplate;
}
}));
// 为参考链接添加事件监听器
containerEl.querySelectorAll('.reference-link').forEach(el => {
el.addEventListener('click', (e) => {
e.preventDefault();
// 这里可以添加打开参考文档的逻辑
});
});
// 更新前置元数据模板设置
const metadataInstructionsFragment = document.createRange().createContextualFragment(metadataVariablesInstructions);
new Setting(containerEl)
.setName('Metadata Variables')
.setDesc(metadataInstructionsFragment)
.addTextArea(text => text
.setPlaceholder('Enter front matter variables')
.setValue(this.plugin.settings.frontMatterVariables.join(','))
.onChange(async (value) => {
this.plugin.settings.frontMatterVariables = value
.split(',')
.map((v) => v.trim())
.filter(
(v, i, a) =>
FRONT_MATTER_VARIABLES.includes(v.split('::')[0]) &&
a.indexOf(v) === i,
)
await this.plugin.saveSettings();
})
// 设置文本区域的大小
.then(textArea => {
textArea.inputEl.rows = 20;
textArea.inputEl.cols = 30;
}))
.addButton(button => button
.setIcon('reset')
.setTooltip('Reset to default')
.onClick(async () => {
this.plugin.settings.frontMatterVariables = DEFAULT_SETTINGS.frontMatterVariables;
await this.plugin.saveSettings();
// 刷新显示
const textArea = button.buttonEl.parentElement?.parentElement?.querySelector('textarea');
if (textArea) {
textArea.value = DEFAULT_SETTINGS.frontMatterVariables.join(',');
}
}));
// 更新内容模板设置
const contentInstructionsFragment = document.createRange().createContextualFragment(contentTemplateInstructions);
new Setting(containerEl)
.setName('Content Template')
.setDesc(contentInstructionsFragment)
.addTextArea(text => text
.setPlaceholder('Enter content template')
.setValue(this.plugin.settings.contentTemplate)
.onChange(async (value) => {
this.plugin.settings.contentTemplate = value;
await this.plugin.saveSettings();
})
// 设置文本区域的大小
.then(textArea => {
textArea.inputEl.rows = 20;
textArea.inputEl.cols = 30;
}))
.addButton(button => button
.setIcon('reset')
.setTooltip('Reset to default')
.onClick(async () => {
this.plugin.settings.contentTemplate = DEFAULT_SETTINGS.contentTemplate;
await this.plugin.saveSettings();
// 刷新显示
const textArea = button.buttonEl.parentElement?.parentElement?.querySelector('textarea');
if (textArea) {
textArea.value = DEFAULT_SETTINGS.contentTemplate;
}
}));
// 更新日期格式模板设置
const dateFormatInstructionsFragment = document.createRange().createContextualFragment(cuboxDateFormat);
new Setting(containerEl)
.setName('Date Format')
.setDesc(dateFormatInstructionsFragment)
.addText(text => text
.setPlaceholder('Enter date format')
.setValue(this.plugin.settings.dateFormat)
.onChange(async (value) => {
this.plugin.settings.dateFormat = value;
await this.plugin.saveSettings();
this.plugin.updateTemplateProcessorDateFormat(value);
}));
// 状态部分
containerEl.createEl('h3', {text: 'Status'});
containerEl.createEl('p', {text: `Last sync: ${this.plugin.formatLastSyncTime()}`});
}
// 添加一个方法来更新 API Key 设置的描述和状态
private updateApiKeySetting(): void {
const domain = this.plugin.settings.domain;
const textComponent = this.apiKeySetting.components[0] as any;
if (!domain) {
// 未选择域名
this.apiKeySetting.setDesc('Please select a region first');
textComponent.inputEl.disabled = true;
} else {
// 已选择域名,更新描述和链接
const url = `https://${domain}/my/settings/extensions`;
const descEl = this.apiKeySetting.descEl;
// 清空现有描述
descEl.empty();
// 添加新的描述文本和链接
descEl.appendChild(
createSpan({text: 'You can create a key in the '})
);
descEl.appendChild(
createEl('a', {
text: 'Extension Settings',
href: url,
attr: {
target: '_blank',
rel: 'noopener'
}
})
);
descEl.appendChild(
createSpan({text: ' of Cubox web app.'})
);
// 启用输入框
textComponent.inputEl.disabled = false;
}
}
// 获取文件夹过滤器按钮文本
private getFolderFilterButtonText(): string {
const folderFilters = this.plugin.settings.folderFilter;
if (!folderFilters || folderFilters.length === 0) {
return 'Manage';
} else if (folderFilters.includes(ALL_FOLDERS_ID)) {
return 'All Folders';
} else {
return `${folderFilters.length} selected`;
}
}
// 修改获取类型过滤器按钮文本的方法
private getTypeFilterButtonText(): string {
const typeFilters = this.plugin.settings.typeFilter;
if (!typeFilters || typeFilters.length === 0) {
return 'Manage';
} else if (typeFilters.length === ALL_CONTENT_TYPES.length) {
return 'All Types';
} else {
return `${typeFilters.length} selected`;
}
}
private getStatusFilterButtonText(): string {
const statusFilters = this.plugin.settings.statusFilter;
if (!statusFilters || statusFilters.length === 0) {
return 'Manage';
} else if (statusFilters.includes('all')) {
return 'All Items';
} else {
return `${statusFilters.length} selected`;
}
}
private getTagFilterButtonText(): string {
const tagFilters = this.plugin.settings.tagsFilter;
if (!tagFilters || tagFilters.length === 0) {
return 'Manage';
} else if (tagFilters.includes(ALL_ITEMS)) {
return 'All Items';
} else if (tagFilters.includes('')) {
return 'No Tags';
} else {
return `${tagFilters.length} selected`;
}
}
}

View file

@ -2,7 +2,6 @@ import { App, Modal, Setting, Notice } from 'obsidian';
import { CuboxFolder } from '../cuboxApi';
import { ModalStyleManager } from './modalStyles';
// 定义一个虚拟的 ALL 文件夹 ID
export const ALL_FOLDERS_ID = 'all_folders';
export class FolderSelectModal extends Modal {
@ -37,11 +36,9 @@ export class FolderSelectModal extends Modal {
async onOpen() {
const { contentEl } = this;
// 设置标题
contentEl.createEl('h2', { text: 'Manage Cubox folders to be synced' });
contentEl.addClass('cubox-folder-select-modal');
// 创建固定的容器结构
this.listEl = contentEl.createDiv({ cls: 'folder-list-container' });
this.footerEl = contentEl.createDiv({ cls: 'modal-footer' });
@ -49,13 +46,11 @@ export class FolderSelectModal extends Modal {
this.createFolderList();
// 添加确认和取消按钮
// 取消按钮
const cancelButton = this.footerEl.createEl('button', { text: 'Cancel' });
cancelButton.addEventListener('click', () => {
this.close();
});
// 确认按钮
const confirmButton = this.footerEl.createEl('button', { text: 'Done', cls: 'mod-cta' });
confirmButton.addEventListener('click', () => {
// 检查是否至少选择了一个选项
@ -87,33 +82,21 @@ export class FolderSelectModal extends Modal {
}
private createFolderList() {
// 清除现有列表
this.listEl.empty();
// 添加"All Items"选项
const allItemsSetting = new Setting(this.listEl)
.setName('All Items');
// 添加选中状态的类
if (this.selectedFolders.has(ALL_FOLDERS_ID)) {
allItemsSetting.settingEl.addClass('is-selected');
}
// 添加点击事件
allItemsSetting.settingEl.addEventListener('click', () => {
const isCurrentlySelected = this.selectedFolders.has(ALL_FOLDERS_ID);
this.handleFolderToggle(ALL_FOLDERS_ID, !isCurrentlySelected);
this.redraw();
});
// 保留原有的toggle但隐藏它通过CSS以保持原有逻辑
allItemsSetting.addToggle(toggle => toggle
.setValue(this.selectedFolders.has(ALL_FOLDERS_ID))
.onChange(value => {
this.handleFolderToggle(ALL_FOLDERS_ID, value);
this.redraw();
}));
// 添加每个文件夹的选项
this.folders.forEach(folder => {
const folderSetting = new Setting(this.listEl)
@ -135,19 +118,11 @@ export class FolderSelectModal extends Modal {
this.redraw();
});
}
// 保留原有的toggle但隐藏它通过CSS以保持原有逻辑
folderSetting.addToggle(toggle => toggle
.setValue(this.selectedFolders.has(folder.id))
.onChange(value => {
this.handleFolderToggle(folder.id, value);
this.redraw();
}));
});
}
private isFolderSelected(folderId: string): boolean {
// 检查特定文件夹是否被选中
return this.selectedFolders.has(folderId);
}
@ -158,18 +133,14 @@ export class FolderSelectModal extends Modal {
this.selectedFolders.clear();
this.selectedFolders.add(ALL_FOLDERS_ID);
} else {
// 如果取消了"All Items",清除它
this.selectedFolders.delete(ALL_FOLDERS_ID);
// 不自动选择其他文件夹,让用户自己选择
}
} else {
if (isSelected) {
// 如果选择了特定文件夹,移除"All Items"
this.selectedFolders.delete(ALL_FOLDERS_ID);
// 添加新选择的文件夹
this.selectedFolders.add(folderId);
} else {
// 移除取消选择的文件夹
this.selectedFolders.delete(folderId);
}
}
@ -183,7 +154,6 @@ export class FolderSelectModal extends Modal {
const { contentEl } = this;
contentEl.empty();
// 使用通用样式管理器移除样式
ModalStyleManager.removeModalStyles('cubox-folder-modal-styles');
}
}

View file

@ -1,6 +1,4 @@
/**
*
*/
export class ModalStyleManager {
/**
*
@ -37,10 +35,7 @@ export class ModalStyleManager {
.modal-footer button {
margin-left: 8px;
}
/* 隐藏原有的 toggle/switch 控件 */
.${listContainerClass} .setting-item-control {
display: none;
}
/* 调整设置项的布局,为左侧的 checkbox 腾出空间 */
.${listContainerClass} .setting-item {
position: relative;

View file

@ -42,7 +42,6 @@ export class StatusSelectModal extends Modal {
if (id) this.selectedStatuses.add(id);
});
} else {
// 如果没有初始选择,默认选中"All Items"
this.selectedStatuses.add(ALL_STATUS_ID);
}
}
@ -50,11 +49,9 @@ export class StatusSelectModal extends Modal {
async onOpen() {
const { contentEl } = this;
// 设置标题和样式
contentEl.createEl('h2', { text: 'Manage Cubox content status to be synced' });
contentEl.addClass('cubox-status-select-modal');
// 创建固定的容器结构
this.listEl = contentEl.createDiv({ cls: 'status-list-container' });
this.footerEl = contentEl.createDiv({ cls: 'modal-footer' });
@ -62,13 +59,11 @@ export class StatusSelectModal extends Modal {
this.createStatusList();
// 添加底部按钮
// 取消按钮
const cancelButton = this.footerEl.createEl('button', { text: 'Cancel' });
cancelButton.addEventListener('click', () => {
this.close();
});
// 保存按钮
const saveButton = this.footerEl.createEl('button', { text: 'Done', cls: 'mod-cta' });
saveButton.addEventListener('click', () => {
// 检查是否至少选择了一个选项
@ -87,12 +82,10 @@ export class StatusSelectModal extends Modal {
this.close();
});
// 添加样式以固定底部按钮
this.addStyles();
}
private addStyles() {
// 使用通用样式管理器添加样式
ModalStyleManager.addModalStyles(
'cubox-status-modal-styles',
'cubox-status-select-modal',
@ -101,7 +94,6 @@ export class StatusSelectModal extends Modal {
}
private createStatusList() {
// 清除现有列表
this.listEl.empty();
// 添加每个状态的选项
@ -116,7 +108,6 @@ export class StatusSelectModal extends Modal {
statusSetting.settingEl.addClass('is-selected');
}
// 添加点击事件
statusSetting.settingEl.addEventListener('click', () => {
const isCurrentlySelected = this.selectedStatuses.has(status.id);
this.handleStatusToggle(status.id, !isCurrentlySelected);
@ -127,12 +118,10 @@ export class StatusSelectModal extends Modal {
if (this.selectedStatuses.has(ALL_STATUS_ID)) {
statusSetting.settingEl.addClass('is-disabled');
} else {
// 添加选中状态的类
if (this.selectedStatuses.has(status.id)) {
statusSetting.settingEl.addClass('is-selected');
}
// 添加点击事件
statusSetting.settingEl.addEventListener('click', () => {
const isCurrentlySelected = this.selectedStatuses.has(status.id);
this.handleStatusToggle(status.id, !isCurrentlySelected);
@ -164,19 +153,15 @@ export class StatusSelectModal extends Modal {
if (isSelected) {
// 如果选择了其他选项,移除"All Items"
this.selectedStatuses.delete(ALL_STATUS_ID);
// 添加新选择的状态
this.selectedStatuses.add(statusId);
// 将所有状态值设为 false然后将选中的状态值设为 true
this.statuses.forEach(status => {
if (status.id !== 'all') {
this.statusValues.set(status.id, status.id === statusId);
}
});
} else {
// 移除取消选择的状态
this.selectedStatuses.delete(statusId);
// 将该状态值设为 false
this.statusValues.set(statusId, false);
}
}
@ -190,7 +175,6 @@ export class StatusSelectModal extends Modal {
const { contentEl } = this;
contentEl.empty();
// 使用通用样式管理器移除样式
ModalStyleManager.removeModalStyles('cubox-status-modal-styles');
}
}

View file

@ -2,7 +2,6 @@ import { App, Modal, Setting, Notice } from 'obsidian';
import { CuboxTag } from '../cuboxApi';
import { ModalStyleManager } from './modalStyles';
// 定义虚拟的 ALL 标签 ID 和 NO_TAGS 标签 ID
export const ALL_ITEMS = 'all_items';
export const NO_TAGS_ID = '';
@ -38,11 +37,9 @@ export class TagSelectModal extends Modal {
async onOpen() {
const { contentEl } = this;
// 设置标题
contentEl.createEl('h2', { text: 'Manage Cubox tags to be synced' });
contentEl.addClass('cubox-tag-select-modal');
// 创建固定的容器结构
this.listEl = contentEl.createDiv({ cls: 'tag-list-container' });
this.footerEl = contentEl.createDiv({ cls: 'modal-footer' });
@ -50,13 +47,11 @@ export class TagSelectModal extends Modal {
this.createTagList();
// 添加确认和取消按钮
// 取消按钮
const cancelButton = this.footerEl.createEl('button', { text: 'Cancel' });
cancelButton.addEventListener('click', () => {
this.close();
});
// 确认按钮
const confirmButton = this.footerEl.createEl('button', { text: 'Done', cls: 'mod-cta' });
confirmButton.addEventListener('click', () => {
// 检查是否至少选择了一个选项
@ -71,12 +66,10 @@ export class TagSelectModal extends Modal {
this.close();
});
// 添加样式
this.addStyles();
}
private addStyles() {
// 使用通用样式管理器添加样式
ModalStyleManager.addModalStyles(
'cubox-tag-modal-styles',
'cubox-tag-select-modal',
@ -85,14 +78,12 @@ export class TagSelectModal extends Modal {
}
private createTagList() {
// 清除现有列表
this.listEl.empty();
// 添加"All Items"选项
const allItemsSetting = new Setting(this.listEl)
.setName('All Items');
// 添加选中状态的类
if (this.selectedTags.has(ALL_ITEMS)) {
allItemsSetting.settingEl.addClass('is-selected');
}
@ -104,14 +95,6 @@ export class TagSelectModal extends Modal {
this.redraw();
});
// 保留原有的toggle但隐藏它通过CSS以保持原有逻辑
allItemsSetting.addToggle(toggle => toggle
.setValue(this.selectedTags.has(ALL_ITEMS))
.onChange(value => {
this.handleTagToggle(ALL_ITEMS, value);
this.redraw();
}));
// 添加"No Tags"选项
const noTagsSetting = new Setting(this.listEl)
.setName('No Tags');
@ -125,7 +108,6 @@ export class TagSelectModal extends Modal {
noTagsSetting.settingEl.addClass('is-selected');
}
// 添加点击事件
noTagsSetting.settingEl.addEventListener('click', () => {
const isCurrentlySelected = this.selectedTags.has(NO_TAGS_ID);
this.handleTagToggle(NO_TAGS_ID, !isCurrentlySelected);
@ -133,14 +115,6 @@ export class TagSelectModal extends Modal {
});
}
// 保留原有的toggle但隐藏它通过CSS以保持原有逻辑
noTagsSetting.addToggle(toggle => toggle
.setValue(this.selectedTags.has(NO_TAGS_ID))
.onChange(value => {
this.handleTagToggle(NO_TAGS_ID, value);
this.redraw();
}));
// 添加每个标签的选项
this.tags.forEach(tag => {
const tagSetting = new Setting(this.listEl)
@ -155,7 +129,6 @@ export class TagSelectModal extends Modal {
tagSetting.settingEl.addClass('is-selected');
}
// 添加点击事件
tagSetting.settingEl.addEventListener('click', () => {
const isCurrentlySelected = this.selectedTags.has(tag.id);
this.handleTagToggle(tag.id, !isCurrentlySelected);
@ -163,18 +136,10 @@ export class TagSelectModal extends Modal {
});
}
// 保留原有的toggle但隐藏它通过CSS以保持原有逻辑
tagSetting.addToggle(toggle => toggle
.setValue(this.selectedTags.has(tag.id))
.onChange(value => {
this.handleTagToggle(tag.id, value);
this.redraw();
}));
});
}
private isTagSelected(tagId: string): boolean {
// 检查特定标签是否被选中
return this.selectedTags.has(tagId);
}
@ -185,18 +150,13 @@ export class TagSelectModal extends Modal {
this.selectedTags.clear();
this.selectedTags.add(ALL_ITEMS);
} else {
// 如果取消了"All Items",清除它
this.selectedTags.delete(ALL_ITEMS);
// 不自动选择其他标签,让用户自己选择
}
} else {
if (isSelected) {
// 如果选择了特定标签,移除"All Items"
this.selectedTags.delete(ALL_ITEMS);
// 添加新选择的标签
this.selectedTags.add(tagId);
} else {
// 移除取消选择的标签
this.selectedTags.delete(tagId);
}
}
@ -210,7 +170,6 @@ export class TagSelectModal extends Modal {
const { contentEl } = this;
contentEl.empty();
// 使用通用样式管理器移除样式
ModalStyleManager.removeModalStyles('cubox-tag-modal-styles');
}
}

View file

@ -1,7 +1,6 @@
import { App, Modal, Setting, Notice } from 'obsidian';
import { ModalStyleManager } from './modalStyles';
// 定义所有可用的内容类型(首字母大写)
export const ALL_CONTENT_TYPES = [
'Article',
'Snippet',
@ -40,26 +39,20 @@ export class TypeSelectModal extends Modal {
const { contentEl } = this;
contentEl.empty();
// 添加标题
contentEl.createEl('h2', { text: 'Select Content Types' });
contentEl.createEl('h2', { text: 'Manage Cubox content types to be synced' });
contentEl.addClass('cubox-type-select-modal');
// 创建类型列表容器
this.listEl = contentEl.createDiv({ cls: 'type-list-container' });
// 创建底部按钮容器
this.footerEl = contentEl.createDiv({ cls: 'modal-footer' });
// 创建类型列表
this.createTypeList();
// 添加取消按钮
const cancelButton = this.footerEl.createEl('button', { text: 'Cancel' });
cancelButton.addEventListener('click', () => {
this.close();
});
// 添加保存按钮
const saveButton = this.footerEl.createEl('button', { text: 'Done', cls: 'mod-cta' });
saveButton.addEventListener('click', () => {
// 检查是否至少选择了一个选项
@ -68,18 +61,15 @@ export class TypeSelectModal extends Modal {
return;
}
// 返回选中的类型
const selectedTypes = Array.from(this.selectedTypes);
this.onSave(selectedTypes);
this.close();
});
// 添加样式
this.addStyles();
}
private addStyles() {
// 使用通用样式管理器添加样式
ModalStyleManager.addModalStyles(
'cubox-modal-styles',
'cubox-type-select-modal',
@ -99,7 +89,6 @@ export class TypeSelectModal extends Modal {
}
private createTypeList() {
// 清除现有列表
this.listEl.empty();
// 添加每个类型的选项
@ -137,10 +126,8 @@ export class TypeSelectModal extends Modal {
const { contentEl } = this;
contentEl.empty();
// 使用通用样式管理器移除样式
ModalStyleManager.removeModalStyles('cubox-modal-styles');
// 移除额外的样式
const additionalStyleEl = document.getElementById('cubox-type-modal-additional-styles');
if (additionalStyleEl) additionalStyleEl.remove();
}

View file

@ -16,7 +16,7 @@ Enter template for creating synced article file name.
`;
export const metadataVariablesInstructions = `
Enter the metadata separated by comma. you can also use custom aliases in the format of metadata::alias.
Enter the metadata separated by comma. you can also use custom aliases in the format of metadata::alias. For syncing purposes, the id will always be included.
<div class="cubox-variables-container">
<div class="cubox-variables-title">Available variables</div>
@ -32,7 +32,6 @@ Enter the metadata separated by comma. you can also use custom aliases in the fo
<li>description</li>
<li>words_count</li>
<li>type</li>
<li>id</li>
</ul>
<div class="cubox-reference">For more, refer to <a href="#" class="reference-link">reference</a>.</div>
</div>

View file

@ -1,8 +1,7 @@
import { CuboxArticle, CuboxHighlight } from './cuboxApi';
import { formatDateTime, generateSafeFileArticleName } from './utils';
import Mustache from 'mustache';
//import * as Mustache from 'mustache';
import { parseYaml, stringifyYaml } from 'obsidian';
import { stringifyYaml } from 'obsidian';
export const FRONT_MATTER_VARIABLES = [
'title',
@ -21,7 +20,6 @@ export const FRONT_MATTER_VARIABLES = [
'id'
]
// Add these interfaces for the view models
interface HighlightView {
id: string;
text: string;
@ -57,11 +55,7 @@ interface ArticleView {
export class TemplateProcessor {
// 存储日期格式
private dateFormat: string = 'yyyy-MM-dd';
/**
*
* @param format
*/
setDateFormat(format: string): void {
this.dateFormat = format;
}
@ -85,7 +79,6 @@ export class TemplateProcessor {
domain: article.domain || '',
type: article.type || '',
id: article.id || '',
// 可以根据需要添加更多字段
};
let filename = '';
@ -123,7 +116,6 @@ export class TemplateProcessor {
}
for (const item of templateVariables) {
// split the item into variable and alias
const aliasedVariables = item.split('::')
const variable = aliasedVariables[0]
if (
@ -137,7 +129,6 @@ export class TemplateProcessor {
const value = (article as any)[variable]
if (value) {
// if variable is in article, use it
frontMatter[variable] = value
}
}
@ -171,9 +162,8 @@ export class TemplateProcessor {
// 1. 创建 ArticleView 对象
const articleView = this.createArticleView(article);
// 2. 检查模板中是否包含 content_highlighted 字段
// 2. 只有当模板中包含 content_highlighted 字段且文章有内容和高亮时才生成高亮内容
if (template.includes('content_highlighted') && article.content && article.highlights && article.highlights.length > 0) {
// 只有当模板中包含 content_highlighted 字段且文章有内容和高亮时才生成高亮内容
articleView.content_highlighted = this.generateHighlightedContent(article.content, article.highlights);
}
@ -221,12 +211,14 @@ export class TemplateProcessor {
// 确保文本和注释正确处理
let text = highlight.text || '';
// 处理多行文本,确保每行都有引用符号
// 将文本按换行符分割,然后在每行前添加引用符号,最后重新组合
if (text.includes('\n')) {
// 分割文本为多行
// 检查是否为图片高亮
if (!text && highlight.image_url) {
// 如果文本为空但有图片URL使用图片作为高亮内容
text = `![](${highlight.image_url})`;
} else if (text.includes('\n')) {
// 处理多行文本,确保每行都有引用符号
// 将文本按换行符分割,然后在每行前添加引用符号,最后重新组合
const lines = text.split('\n');
// 为每行添加引用符号(第一行保持原样,因为模板中已经有 > 符号)
const formattedLines = lines.map((line, index) => {
// 第一行不需要添加引用符号
if (index === 0) return line;