diff --git a/src/cuboxApi.ts b/src/cuboxApi.ts index 6a3e74b..da7a6fe 100644 --- a/src/cuboxApi.ts +++ b/src/cuboxApi.ts @@ -147,6 +147,7 @@ export class CuboxApi { folderFilter?: string[]; typeFilter?: string[]; statusFilter?: string[]; + tagsFilter?: string[]; } = { lastCardId: null } ): Promise<{ articles: CuboxArticle[], hasMore: boolean, lastCardId: string | null }> { try { @@ -182,6 +183,14 @@ export class CuboxApi { if (params.statusFilter.includes('annotated')) searchParams.append('isAnnotated', 'true'); } } + + // 添加标签过滤 + if (params.tagsFilter && params.tagsFilter.length > 0) { + params.tagsFilter.forEach(tag => { + // 如果是空字符串,表示"No Tags" + searchParams.append('tagId', tag); + }); + } const path = `/c/api/third-party/card/list?${searchParams.toString()}`; const response = await this.request(path) as ListResponse; diff --git a/src/main.ts b/src/main.ts index ac180b0..5d4dfbc 100644 --- a/src/main.ts +++ b/src/main.ts @@ -6,6 +6,7 @@ import { FolderSelectModal } from './modal/folderSelectModal'; import { filenameTemplateInstructions, metadataTemplateInstructions, contentTemplateInstructions } from './templateInstructions'; import { TypeSelectModal } from './modal/typeSelectModal'; import { StatusSelectModal } from './modal/statusSelectModal'; +import { TagSelectModal } from './modal/tagSelectModal'; interface CuboxSyncSettings { domain: string; // 可以是 'cubox.cc' | 'cubox.pro' | '' @@ -13,6 +14,7 @@ interface CuboxSyncSettings { folderFilter: string[]; typeFilter: string[]; statusFilter: string[]; + tagsFilter: string[]; // 添加标签过滤 syncFrequency: number; targetFolder: string; filenameTemplate: string; @@ -31,6 +33,7 @@ const DEFAULT_SETTINGS: CuboxSyncSettings = { folderFilter: [], typeFilter: [], statusFilter: ['all'], + tagsFilter: [], // 默认为空数组,表示同步所有标签 syncFrequency: 30, // 分钟 targetFolder: 'Cubox', filenameTemplate: '{{cardTitle}}-{{createDate}}', @@ -199,7 +202,8 @@ export default class CuboxSyncPlugin extends Plugin { lastCardId: lastCardId, folderFilter: this.settings.folderFilter, typeFilter: this.settings.typeFilter, - statusFilter: this.settings.statusFilter + statusFilter: this.settings.statusFilter, + tagsFilter: this.settings.tagsFilter, } ); @@ -426,7 +430,47 @@ class CuboxSyncSettingTab extends PluginSettingTab { } })); - + 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('请先设置 API Key'); + 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('获取标签列表失败,请检查网络连接和 API Key'); + // 恢复按钮文本 + button.setButtonText(this.getTagFilterButtonText()); + } + })); + new Setting(containerEl) .setName('Type Filter') .setDesc('Manage Cubox content types to be synced') @@ -695,4 +739,14 @@ class CuboxSyncSettingTab extends PluginSettingTab { return `已选择 ${statusFilters.length} 个状态`; } } + + private getTagFilterButtonText(): string { + if (!this.plugin.settings.tagsFilter || this.plugin.settings.tagsFilter.length === 0) { + return 'All Tags'; + } else if (this.plugin.settings.tagsFilter.includes('')) { + return 'No Tags'; + } else { + return `${this.plugin.settings.tagsFilter.length} Tags Selected`; + } + } } diff --git a/src/modal/statusSelectModal.ts b/src/modal/statusSelectModal.ts index 94df225..1321a47 100644 --- a/src/modal/statusSelectModal.ts +++ b/src/modal/statusSelectModal.ts @@ -11,7 +11,6 @@ export class StatusSelectModal extends Modal { { id: 'all', name: 'All Items' }, { id: 'read', name: 'Already Read Items' }, { id: 'starred', name: 'Starred Items' }, - { id: 'archived', name: 'Archived Items' }, { id: 'annotated', name: 'Annotated Items' } ]; private selectedStatuses: Set = new Set(); @@ -57,7 +56,7 @@ export class StatusSelectModal extends Modal { }); // 保存按钮 - const saveButton = this.footerEl.createEl('button', { text: '保存', cls: 'mod-cta' }); + const saveButton = this.footerEl.createEl('button', { text: '确认', cls: 'mod-cta' }); saveButton.addEventListener('click', () => { // 返回选中的状态 const selectedIds = Array.from(this.selectedStatuses); diff --git a/src/modal/tagSelectModal.ts b/src/modal/tagSelectModal.ts new file mode 100644 index 0000000..6fdbed4 --- /dev/null +++ b/src/modal/tagSelectModal.ts @@ -0,0 +1,215 @@ +import { App, Modal, Setting, Notice } from 'obsidian'; +import { CuboxTag } from '../cuboxApi'; +import { ModalStyleManager } from '../utils/modalStyles'; + +// 定义虚拟的 ALL 标签 ID 和 NO_TAGS 标签 ID +const ALL_TAGS_ID = 'all_tags'; +const NO_TAGS_ID = 'no_tags'; + +export class TagSelectModal extends Modal { + private selectedTags: Set = new Set(); + private onConfirm: (selectedTags: string[]) => void; + private tags: CuboxTag[] = []; + private listEl: HTMLElement; + private footerEl: HTMLElement; + + constructor( + app: App, + tags: CuboxTag[], + initialSelectedTags: string[], + onConfirm: (selectedTags: string[]) => void + ) { + super(app); + this.tags = tags; + + // 初始化已选择的标签 + if (initialSelectedTags && initialSelectedTags.length > 0) { + initialSelectedTags.forEach(id => { + // 特殊处理空字符串,将其转换为 NO_TAGS_ID + if (id === '') { + this.selectedTags.add(NO_TAGS_ID); + } else if (id) { + this.selectedTags.add(id); + } + }); + } else { + // 如果没有初始选择,默认选中"All Items" + this.selectedTags.add(ALL_TAGS_ID); + this.selectedTags.add(NO_TAGS_ID); + } + + this.onConfirm = onConfirm; + } + + 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' }); + + // 创建标签列表 + this.createTagList(); + + // 添加确认和取消按钮 + // 取消按钮 + const cancelButton = this.footerEl.createEl('button', { text: '取消' }); + cancelButton.addEventListener('click', () => { + this.close(); + }); + + // 确认按钮 + const confirmButton = this.footerEl.createEl('button', { text: '确认', cls: 'mod-cta' }); + confirmButton.addEventListener('click', () => { + // 处理选中的标签 + let resultTags: string[] = []; + + // 如果选中了ALL_TAGS_ID,则传递空数组表示同步所有内容 + if (this.selectedTags.has(ALL_TAGS_ID)) { + resultTags = []; + } else { + // 将选中的标签ID添加到结果中 + resultTags = Array.from(this.selectedTags); + + // 特殊处理 NO_TAGS_ID,将其转换为空字符串 + if (resultTags.includes(NO_TAGS_ID)) { + // 移除 NO_TAGS_ID + resultTags = resultTags.filter(id => id !== NO_TAGS_ID); + // 添加空字符串表示无标签 + resultTags.push(''); + } + } + + this.onConfirm(resultTags); + this.close(); + }); + + // 添加样式 + this.addStyles(); + } + + private addStyles() { + // 使用通用样式管理器添加样式 + ModalStyleManager.addModalStyles( + 'cubox-tag-modal-styles', + 'cubox-tag-select-modal', + 'tag-list-container' + ); + } + + private createTagList() { + // 清除现有列表 + this.listEl.empty(); + + // 添加"All Items"选项 + new Setting(this.listEl) + .setName('All Items') + .addToggle(toggle => toggle + .setValue(this.selectedTags.has(ALL_TAGS_ID)) + .onChange(value => { + this.handleTagToggle(ALL_TAGS_ID, value); + this.redraw(); + })); + + // 添加"No Tags"选项 + new Setting(this.listEl) + .setName('No Tags') + .addToggle(toggle => toggle + .setValue(this.selectedTags.has(NO_TAGS_ID)) + .onChange(value => { + this.handleTagToggle(NO_TAGS_ID, value); + this.redraw(); + })); + + // 添加每个标签的选项 + this.tags.forEach(tag => { + new Setting(this.listEl) + .setName(tag.nested_name) + .addToggle(toggle => toggle + .setValue(this.isTagSelected(tag.id)) + .onChange(value => { + this.handleTagToggle(tag.id, value); + this.redraw(); + })); + }); + } + + private isTagSelected(tagId: string): boolean { + // 如果选中了ALL_TAGS_ID,则所有标签都被选中 + if (this.selectedTags.has(ALL_TAGS_ID)) { + return true; + } + // 否则检查特定标签是否被选中 + return this.selectedTags.has(tagId); + } + + private handleTagToggle(tagId: string, isSelected: boolean) { + if (tagId === ALL_TAGS_ID) { + if (isSelected) { + // 如果选择了"All Items",清除其他所有选择,只保留ALL_TAGS_ID + this.selectedTags.clear(); + this.selectedTags.add(ALL_TAGS_ID); + this.selectedTags.add(NO_TAGS_ID); + } else { + // 如果取消了"All Items"且没有其他选择,选中第一个标签 + this.selectedTags.delete(ALL_TAGS_ID); + if (this.selectedTags.size === 0) { + // 如果有标签,选择第一个标签,否则选择"No Tags" + if (this.tags.length > 0) { + this.selectedTags.add(this.tags[0].id); + } else { + this.selectedTags.add(NO_TAGS_ID); + } + } + } + } else { + if (isSelected) { + // 添加新选择的标签 + this.selectedTags.add(tagId); + + // 检查是否所有标签都被选中,如果是,自动选中"All Items" + if (this.areAllTagsSelected()) { + this.selectedTags.clear(); + this.selectedTags.add(ALL_TAGS_ID); + } + } else { + // 如果取消选择某个标签,则取消"All Items"的选择 + this.selectedTags.delete(ALL_TAGS_ID); + + // 正常移除取消选择的标签 + this.selectedTags.delete(tagId); + + // 如果没有任何选择,默认选中"All Items" + if (this.selectedTags.size === 0) { + this.selectedTags.add(ALL_TAGS_ID); + } + } + } + } + + // 检查是否所有标签都被选中(包括"No Tags") + private areAllTagsSelected(): boolean { + // 如果没有标签,返回false + if (this.tags.length === 0) return false; + + // 检查每个标签是否都在selectedTags中,并且"No Tags"也被选中 + return this.tags.every(tag => this.selectedTags.has(tag.id)) && + this.selectedTags.has(NO_TAGS_ID); + } + + private redraw() { + this.createTagList(); + } + + onClose() { + const { contentEl } = this; + contentEl.empty(); + + // 使用通用样式管理器移除样式 + ModalStyleManager.removeModalStyles('cubox-tag-modal-styles'); + } +} \ No newline at end of file diff --git a/src/modal/typeSelectModal.ts b/src/modal/typeSelectModal.ts index 92824f8..d51cc79 100644 --- a/src/modal/typeSelectModal.ts +++ b/src/modal/typeSelectModal.ts @@ -71,13 +71,13 @@ export class TypeSelectModal extends Modal { const footerEl = contentEl.createDiv({ cls: 'modal-footer' }); // 添加取消按钮 - const cancelButton = footerEl.createEl('button', { text: 'Cancel' }); + const cancelButton = footerEl.createEl('button', { text: '取消' }); cancelButton.addEventListener('click', () => { this.close(); }); // 添加保存按钮 - const saveButton = footerEl.createEl('button', { text: 'Save', cls: 'mod-cta' }); + const saveButton = footerEl.createEl('button', { text: '确认', cls: 'mod-cta' }); saveButton.addEventListener('click', () => { // 过滤掉"All Items",只保留实际类型 const selectedTypes = Array.from(this.selectedTypes)