diff --git a/src/cuboxApi.ts b/src/cuboxApi.ts index 74e9894..b1f7a97 100644 --- a/src/cuboxApi.ts +++ b/src/cuboxApi.ts @@ -2,6 +2,7 @@ import { Notice, requestUrl } from 'obsidian'; import { ALL_FOLDERS_ID } from './modal/folderSelectModal'; import { ALL_ITEMS } from './modal/tagSelectModal'; import { ALL_STATUS_ID } from './modal/statusSelectModal'; +import { expandTypeFilterForApi } from './modal/typeSelectModal'; export interface CuboxArticle { id: string; @@ -152,9 +153,9 @@ export class CuboxApi { } } - // 添加类型过滤 + // 添加类型过滤(Others 展开为 Memo/Image/Audio/Video/File) if (params.typeFilter && params.typeFilter.length > 0) { - requestBody.type_filters = params.typeFilter; + requestBody.type_filters = expandTypeFilterForApi(params.typeFilter); } // 添加状态过滤 diff --git a/src/cuboxSetting.ts b/src/cuboxSetting.ts index 560306d..f114270 100644 --- a/src/cuboxSetting.ts +++ b/src/cuboxSetting.ts @@ -3,7 +3,7 @@ import type CuboxSyncPlugin from './main'; import { FRONT_MATTER_VARIABLES } from './templateProcessor'; import { ALL_FOLDERS_ID, FolderSelectModal } from './modal/folderSelectModal'; import { filenameTemplateInstructions, metadataVariablesInstructions, contentTemplateInstructions, cuboxDateFormat, getFilenameReferenceLink, getMetadataReferenceLink, getContentReferenceLink, getDateReferenceLink, getInstructionUrl } from './templateInstructions'; -import { ALL_CONTENT_TYPES, TypeSelectModal } from './modal/typeSelectModal'; +import { isFullTypeSelection, storedTypesToModalSelection, TypeSelectModal } from './modal/typeSelectModal'; import { ALL_STATUS_ID, StatusSelectModal } from './modal/statusSelectModal'; import { ALL_ITEMS, TagSelectModal } from './modal/tagSelectModal'; import { normalizePath } from 'obsidian'; @@ -266,13 +266,14 @@ export class CuboxSyncSettingTab extends PluginSettingTab { // 打开类型选择模态框 const modal = new TypeSelectModal( this.app, - this.plugin.settings.typeFilter, + storedTypesToModalSelection(this.plugin.settings.typeFilter), async (selectedTypes) => { this.plugin.settings.typeFilter = selectedTypes; await this.plugin.saveSettings(); // 更新按钮文本 button.setButtonText(this.getTypeFilterButtonText()); - } + }, + this.plugin.settings.domain ); modal.open(); })); @@ -578,7 +579,7 @@ export class CuboxSyncSettingTab extends PluginSettingTab { const typeFilters = this.plugin.settings.typeFilter; if (!typeFilters || typeFilters.length === 0) { return 'Select...'; - } else if (typeFilters.length === ALL_CONTENT_TYPES.length) { + } else if (isFullTypeSelection(typeFilters)) { return 'All types'; } else { return `${typeFilters.length} selected`; diff --git a/src/main.ts b/src/main.ts index 7283025..697fc9f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3,6 +3,7 @@ import { CuboxApi } from './cuboxApi'; import { TemplateProcessor } from './templateProcessor'; import { formatDateTime } from './utils'; import { CuboxSyncSettingTab, CuboxSyncSettings, DEFAULT_SETTINGS } from './cuboxSetting'; +import { normalizeTypeFilterStorage } from './modal/typeSelectModal'; export default class CuboxSyncPlugin extends Plugin { @@ -54,6 +55,7 @@ export default class CuboxSyncPlugin extends Plugin { async loadSettings() { this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); + this.settings.typeFilter = normalizeTypeFilterStorage(this.settings.typeFilter); } async saveSettings() { diff --git a/src/modal/typeSelectModal.ts b/src/modal/typeSelectModal.ts index 7d8c680..10ece0b 100644 --- a/src/modal/typeSelectModal.ts +++ b/src/modal/typeSelectModal.ts @@ -1,29 +1,92 @@ -import { App, Modal, Setting, Notice } from 'obsidian'; +import { App, Modal, Setting, Notice, setIcon } from 'obsidian'; -export const ALL_CONTENT_TYPES = [ - 'Article', - 'Snippet', - 'Memo', - 'Image', - 'Audio', - 'Video', - 'File' -]; +/** API 层使用的五个类型,在 UI 中合并为 Others */ +export const OTHERS_MEMBER_TYPES = ['Memo', 'Image', 'Audio', 'Video', 'File'] as const; + +const OTHERS_MEMBER_SET = new Set(OTHERS_MEMBER_TYPES); + +/** 配置中用于表示上述五类合并项的标识 */ +export const OTHERS_TYPE_ID = 'Others'; + +export const ALL_CONTENT_TYPES = ['Article', 'Snippet', OTHERS_TYPE_ID]; + +/** + * 读盘或读入的 typeFilter 规范化:将历史配置中的 Memo/Image/... 合并为 Others + */ +export function normalizeTypeFilterStorage(typeFilter: string[] | undefined): string[] { + if (!typeFilter || typeFilter.length === 0) return []; + const hasLegacyOthers = OTHERS_MEMBER_TYPES.some((t) => typeFilter.includes(t)); + const next = typeFilter.filter((t) => !OTHERS_MEMBER_SET.has(t)); + if (hasLegacyOthers && !next.includes(OTHERS_TYPE_ID)) { + next.push(OTHERS_TYPE_ID); + } + return next; +} + +/** + * 任意历史成员被选中则视为 Others 选中,供类型选择弹窗初始状态使用 + */ +export function storedTypesToModalSelection(stored: string[]): string[] { + const result = new Set(); + let othersSelected = false; + for (const t of stored) { + if (OTHERS_MEMBER_SET.has(t)) { + othersSelected = true; + } else if (t === OTHERS_TYPE_ID) { + othersSelected = true; + } else { + result.add(t); + } + } + if (othersSelected) { + result.add(OTHERS_TYPE_ID); + } + return Array.from(result); +} + +/** 发起 API 请求时把 Others 展开为五个具体 type_filters */ +export function expandTypeFilterForApi(stored: string[]): string[] { + const out: string[] = []; + for (const t of stored) { + if (t === OTHERS_TYPE_ID) { + out.push(...OTHERS_MEMBER_TYPES); + } else { + out.push(t); + } + } + return [...new Set(out)]; +} + +/** Article + Snippet + Others(或历史上五个子类全选)视为「全部类型」 */ +export function isFullTypeSelection(stored: string[]): boolean { + if (!stored || stored.length === 0) return false; + const hasArticle = stored.includes('Article'); + const hasSnippet = stored.includes('Snippet'); + const hasOthers = + stored.includes(OTHERS_TYPE_ID) || + OTHERS_MEMBER_TYPES.every((t) => stored.includes(t)); + return hasArticle && hasSnippet && hasOthers; +} export class TypeSelectModal extends Modal { private onSave: (selectedTypes: string[]) => void; private selectedTypes: Set = new Set(); private listEl: HTMLElement; private footerEl: HTMLElement; + private domain: string; - constructor(app: App, initialSelected: string[] = [], onSave: (selectedTypes: string[]) => void) { + constructor( + app: App, + initialSelected: string[] = [], + onSave: (selectedTypes: string[]) => void, + domain: string = '' + ) { super(app); this.onSave = onSave; - - // 初始化已选择的类型 + this.domain = domain; + if (initialSelected && initialSelected.length > 0) { - // 添加初始选择的类型 - initialSelected.forEach(id => { + initialSelected.forEach((id) => { if (id) this.selectedTypes.add(id); }); } @@ -32,29 +95,27 @@ export class TypeSelectModal extends Modal { onOpen() { const { contentEl } = this; contentEl.empty(); - + contentEl.createEl('h2', { text: 'Manage Cubox content types to be synced' }); contentEl.addClass('cubox-modal'); - + this.listEl = contentEl.createDiv({ cls: 'type-list-container cubox-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', () => { - // 检查是否至少选择了一个选项 if (this.selectedTypes.size === 0) { new Notice('Please select at least one option.'); return; } - + const selectedTypes = Array.from(this.selectedTypes); this.onSave(selectedTypes); this.close(); @@ -63,18 +124,39 @@ export class TypeSelectModal extends Modal { private createTypeList() { this.listEl.empty(); - - // 添加每个类型的选项 - ALL_CONTENT_TYPES.forEach(typeId => { - new Setting(this.listEl) - .setName(typeId) - .addToggle(toggle => { - toggle.setValue(this.selectedTypes.has(typeId)); - toggle.onChange(value => { - this.handleTypeToggle(typeId, value); - this.redraw(); - }); + + ALL_CONTENT_TYPES.forEach((typeId) => { + const row = new Setting(this.listEl); + if (typeId === OTHERS_TYPE_ID) { + const nameFrag = document.createDocumentFragment(); + const label = document.createElement('span'); + label.textContent = OTHERS_TYPE_ID; + nameFrag.appendChild(label); + const infoEl = document.createElement('span'); + infoEl.className = 'cubox-others-info-icon'; + infoEl.setAttribute('aria-label', 'About Memo and file types'); + setIcon(infoEl, 'info'); + infoEl.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); + const url = + this.domain === 'cubox.pro' + ? 'https://story.cubox.pro/memo-file-update' + : 'https://cubox.cc/blog/memo-file-update/'; + window.open(url); }); + nameFrag.appendChild(infoEl); + row.setName(nameFrag); + } else { + row.setName(typeId); + } + row.addToggle((toggle) => { + toggle.setValue(this.selectedTypes.has(typeId)); + toggle.onChange((value) => { + this.handleTypeToggle(typeId, value); + this.redraw(); + }); + }); }); } @@ -94,4 +176,4 @@ export class TypeSelectModal extends Modal { const { contentEl } = this; contentEl.empty(); } -} \ No newline at end of file +} diff --git a/styles.css b/styles.css index 1d0a6c3..ec3234a 100644 --- a/styles.css +++ b/styles.css @@ -108,4 +108,23 @@ If your plugin does not need CSS, delete this file. .type-list-container .setting-item:first-child .setting-item-name { font-weight: normal; +} + +.cubox-others-info-icon { + display: inline-flex; + align-items: center; + justify-content: center; + margin-left: 6px; + vertical-align: middle; + cursor: var(--cursor); + color: var(--text-muted); +} + +.cubox-others-info-icon:hover { + color: var(--text-accent); +} + +.cubox-others-info-icon svg { + width: 14px; + height: 14px; } \ No newline at end of file