feat: enhance type selection modal with new info icon and improve type filter handling

This commit is contained in:
delphi 2026-04-14 16:29:24 +08:00
parent 618137dfca
commit 202cb56275
5 changed files with 146 additions and 41 deletions

View file

@ -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);
}
// 添加状态过滤

View file

@ -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`;

View file

@ -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() {

View file

@ -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<string>(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<string>();
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<string> = 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();
}
}
}

View file

@ -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;
}