mirror of
https://github.com/olcubo/obsidian-cubox.git
synced 2026-07-22 05:43:25 +00:00
organize directory/fix sync bug
This commit is contained in:
parent
cc091591b6
commit
4d8c156f4f
11 changed files with 617 additions and 877 deletions
187
README.md
187
README.md
|
|
@ -1,171 +1,48 @@
|
|||
# Obsidian Cubox Sync
|
||||
# Cubox Plugin for Obsidian
|
||||
|
||||

|
||||
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/).
|
||||
|
|
@ -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
563
src/cuboxSetting.ts
Normal 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`;
|
||||
}
|
||||
}
|
||||
}
|
||||
596
src/main.ts
596
src/main.ts
|
|
@ -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`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 = ``;
|
||||
} else if (text.includes('\n')) {
|
||||
// 处理多行文本,确保每行都有引用符号
|
||||
// 将文本按换行符分割,然后在每行前添加引用符号,最后重新组合
|
||||
const lines = text.split('\n');
|
||||
// 为每行添加引用符号(第一行保持原样,因为模板中已经有 > 符号)
|
||||
const formattedLines = lines.map((line, index) => {
|
||||
// 第一行不需要添加引用符号
|
||||
if (index === 0) return line;
|
||||
|
|
|
|||
Loading…
Reference in a new issue