mirror of
https://github.com/olcubo/obsidian-cubox.git
synced 2026-07-22 05:43:25 +00:00
filter logic
This commit is contained in:
parent
9e37aa7bfb
commit
a1568cc50f
9 changed files with 713 additions and 218 deletions
105
src/cuboxApi.ts
105
src/cuboxApi.ts
|
|
@ -37,6 +37,7 @@ export interface CuboxApiOptions {
|
|||
export interface CuboxFolder {
|
||||
id: string;
|
||||
name: string;
|
||||
nested_name: string;
|
||||
}
|
||||
|
||||
interface ListResponse {
|
||||
|
|
@ -110,74 +111,65 @@ export class CuboxApi {
|
|||
|
||||
/**
|
||||
* 获取文章列表
|
||||
* @param folders 文件夹过滤数组
|
||||
* @param type 类型过滤
|
||||
* @param status 状态过滤 'all' | 'read' | 'starred' | 'annotated'
|
||||
* @param folderFilter 文件夹过滤数组
|
||||
* @param typeFilter 类型过滤
|
||||
* @param statusFilter 状态过滤 'all' | 'read' | 'starred' | 'annotated'
|
||||
* @param lastCardId 上一页最后一篇文章的ID
|
||||
*/
|
||||
async getArticles(
|
||||
folders: string[] = [],
|
||||
type?: string,
|
||||
status?: string,
|
||||
lastCardId: string | null = null,
|
||||
pageSize: number = 50
|
||||
): Promise<{
|
||||
articles: CuboxArticle[],
|
||||
hasMore: boolean,
|
||||
lastCardId: string | null
|
||||
}> {
|
||||
folderFilter: string[],
|
||||
typeFilter: string[],
|
||||
statusFilter: string[],
|
||||
lastCardId: string | null = null
|
||||
): Promise<{ articles: any[], hasMore: boolean, lastCardId: string | null }> {
|
||||
try {
|
||||
const searchParams = new URLSearchParams();
|
||||
// 构建请求参数
|
||||
const params: any = {
|
||||
limit: 20,
|
||||
};
|
||||
|
||||
if (lastCardId !== null && lastCardId.length > 0) {
|
||||
searchParams.append('lastCardId', lastCardId);
|
||||
}
|
||||
searchParams.append('pageSize', pageSize.toString());
|
||||
|
||||
// 如果有选择文件夹,添加文件夹过滤参数
|
||||
if (folders && folders.length > 0) {
|
||||
searchParams.append('folders', folders.join(','));
|
||||
// 添加文件夹过滤
|
||||
if (folderFilter && folderFilter.length > 0) {
|
||||
params.folder_id = folderFilter;
|
||||
}
|
||||
|
||||
// 添加状态过滤参数
|
||||
if (status && status !== 'all') {
|
||||
switch (status) {
|
||||
case 'read':
|
||||
searchParams.append('isRead', 'true');
|
||||
break;
|
||||
case 'starred':
|
||||
searchParams.append('hasStar', 'true');
|
||||
break;
|
||||
case 'annotated':
|
||||
searchParams.append('hasAnnotation', 'true');
|
||||
break;
|
||||
// 添加类型过滤
|
||||
if (typeFilter && typeFilter.length > 0) {
|
||||
params.type = typeFilter;
|
||||
}
|
||||
|
||||
// 添加状态过滤 - 修改为支持多选
|
||||
if (statusFilter && statusFilter.length > 0) {
|
||||
// 如果包含 'all',则不添加状态过滤
|
||||
if (!statusFilter.includes('all')) {
|
||||
// 处理多个状态过滤
|
||||
if (statusFilter.includes('read')) params.is_read = true;
|
||||
if (statusFilter.includes('starred')) params.is_starred = true;
|
||||
if (statusFilter.includes('archived')) params.is_archived = true;
|
||||
if (statusFilter.includes('annotated')) params.is_annotated = true;
|
||||
}
|
||||
}
|
||||
|
||||
const path = `/c/api/third-party/card/list?${searchParams.toString()}`;
|
||||
const response = await this.request(path) as ListResponse;
|
||||
|
||||
const articles = response.data.list.map(article => {
|
||||
// 添加兼容字段,并格式化日期
|
||||
// 添加分页参数
|
||||
if (lastCardId) {
|
||||
params.last_id = lastCardId;
|
||||
}
|
||||
|
||||
// 发送请求
|
||||
const response = await this.request('/v2/article/list', params);
|
||||
|
||||
// 处理响应
|
||||
if (response && response.data) {
|
||||
return {
|
||||
...article,
|
||||
id: article.cardId,
|
||||
createDate: article.createTime ? formatISODateTime(article.createTime) : '',
|
||||
// 其他可能需要的兼容字段
|
||||
articles: response.data.data || [],
|
||||
hasMore: response.data.has_more || false,
|
||||
lastCardId: response.data.last_id || null
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const hasMore = articles.length >= pageSize;
|
||||
const newLastCardId = articles.length > 0 ? articles[articles.length - 1].cardId : null;
|
||||
|
||||
return {
|
||||
articles,
|
||||
hasMore,
|
||||
lastCardId: newLastCardId
|
||||
};
|
||||
return { articles: [], hasMore: false, lastCardId: null };
|
||||
} catch (error) {
|
||||
console.error('获取 Cubox 文章失败:', error);
|
||||
new Notice('获取 Cubox 文章失败');
|
||||
console.error('获取文章列表失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
|
@ -224,7 +216,10 @@ export class CuboxApi {
|
|||
const path = '/c/api/third-party/folder/list';
|
||||
const response = await this.request(path) as FoldersResponse;
|
||||
|
||||
return response.data.list || [];
|
||||
return response.data.list.map(folder => ({
|
||||
...folder,
|
||||
nested_name: folder.name
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('获取 Cubox 文件夹列表失败:', error);
|
||||
new Notice('获取 Cubox 文件夹列表失败');
|
||||
|
|
|
|||
|
|
@ -1,127 +0,0 @@
|
|||
import { App, Modal, Setting } from 'obsidian';
|
||||
import { CuboxApi } from './cuboxApi';
|
||||
|
||||
export interface CuboxFolder {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export class FolderSelectModal extends Modal {
|
||||
private folders: CuboxFolder[] = [];
|
||||
private selectedFolders: Set<string> = new Set();
|
||||
private onSave: (selectedFolders: string[]) => void;
|
||||
private cuboxApi: CuboxApi;
|
||||
|
||||
constructor(app: App, cuboxApi: CuboxApi, initialSelected: string[], onSave: (selectedFolders: string[]) => void) {
|
||||
super(app);
|
||||
this.cuboxApi = cuboxApi;
|
||||
this.onSave = onSave;
|
||||
|
||||
// 初始化已选择的文件夹
|
||||
initialSelected.forEach(id => {
|
||||
if (id) this.selectedFolders.add(id);
|
||||
});
|
||||
}
|
||||
|
||||
async onOpen() {
|
||||
const { contentEl } = this;
|
||||
|
||||
// 设置标题
|
||||
contentEl.createEl('h2', { text: 'Cubox: 管理要同步的文件夹' });
|
||||
|
||||
try {
|
||||
// 显示加载中提示
|
||||
const loadingEl = contentEl.createEl('p', { text: '加载文件夹列表...' });
|
||||
|
||||
// 获取文件夹列表
|
||||
this.folders = await this.cuboxApi.getFolders();
|
||||
|
||||
// 移除加载提示
|
||||
loadingEl.remove();
|
||||
|
||||
// 如果没有文件夹,显示提示
|
||||
if (this.folders.length === 0) {
|
||||
contentEl.createEl('p', { text: '没有找到文件夹' });
|
||||
return;
|
||||
}
|
||||
|
||||
// 添加"全选"选项
|
||||
new Setting(contentEl)
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.isAllSelected())
|
||||
.onChange(value => {
|
||||
if (value) {
|
||||
// 全选
|
||||
this.folders.forEach(folder => this.selectedFolders.add(folder.id));
|
||||
} else {
|
||||
// 全不选
|
||||
this.selectedFolders.clear();
|
||||
}
|
||||
// 刷新界面
|
||||
this.redraw();
|
||||
}))
|
||||
.setName('全部文件夹');
|
||||
|
||||
// 创建文件夹列表
|
||||
this.createFolderList();
|
||||
|
||||
// 添加底部按钮
|
||||
const footerEl = contentEl.createDiv('modal-footer');
|
||||
|
||||
// 取消按钮
|
||||
const cancelButton = footerEl.createEl('button', { text: '取消' });
|
||||
cancelButton.addEventListener('click', () => {
|
||||
this.close();
|
||||
});
|
||||
|
||||
// 保存按钮
|
||||
const saveButton = footerEl.createEl('button', { text: '保存', cls: 'mod-cta' });
|
||||
saveButton.addEventListener('click', () => {
|
||||
this.onSave(Array.from(this.selectedFolders));
|
||||
this.close();
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('加载文件夹失败:', error);
|
||||
contentEl.createEl('p', { text: '加载文件夹失败,请检查网络连接和 API 密钥' });
|
||||
}
|
||||
}
|
||||
|
||||
private createFolderList() {
|
||||
const { contentEl } = this;
|
||||
|
||||
// 清除现有列表
|
||||
const existingList = contentEl.querySelector('.folder-list');
|
||||
if (existingList) existingList.remove();
|
||||
|
||||
// 创建新列表容器
|
||||
const listEl = contentEl.createDiv({ cls: 'folder-list' });
|
||||
|
||||
// 添加每个文件夹的选项
|
||||
this.folders.forEach(folder => {
|
||||
new Setting(listEl)
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.selectedFolders.has(folder.id))
|
||||
.onChange(value => {
|
||||
if (value) {
|
||||
this.selectedFolders.add(folder.id);
|
||||
} else {
|
||||
this.selectedFolders.delete(folder.id);
|
||||
}
|
||||
}))
|
||||
.setName(folder.name);
|
||||
});
|
||||
}
|
||||
|
||||
private isAllSelected(): boolean {
|
||||
return this.folders.length > 0 && this.folders.every(folder => this.selectedFolders.has(folder.id));
|
||||
}
|
||||
|
||||
private redraw() {
|
||||
this.createFolderList();
|
||||
}
|
||||
|
||||
onClose() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
}
|
||||
165
src/main.ts
165
src/main.ts
|
|
@ -2,15 +2,17 @@ import { App, Editor, MarkdownView, Modal, Notice, Plugin, PluginSettingTab, Set
|
|||
import { CuboxApi, CuboxApiOptions } from './cuboxApi';
|
||||
import { TemplateProcessor } from './templateProcessor';
|
||||
import { formatISODateTime, getCurrentFormattedTime } from './utils';
|
||||
import { FolderSelectModal } from './folderSelectModal';
|
||||
import { FolderSelectModal } from './modal/folderSelectModal';
|
||||
import { filenameTemplateInstructions, metadataTemplateInstructions, contentTemplateInstructions } from './templateInstructions';
|
||||
import { TypeSelectModal } from './modal/typeSelectModal';
|
||||
import { StatusSelectModal } from './modal/statusSelectModal';
|
||||
|
||||
interface CuboxSyncSettings {
|
||||
domain: string; // 可以是 'cubox.cc' | 'cubox.pro' | ''
|
||||
apiKey: string;
|
||||
folderFilter: string[];
|
||||
typeFilter: string;
|
||||
statusFilter: 'all' | 'read' | 'starred' | 'annotated';
|
||||
typeFilter: string[];
|
||||
statusFilter: string[];
|
||||
syncFrequency: number;
|
||||
targetFolder: string;
|
||||
filenameTemplate: string;
|
||||
|
|
@ -20,14 +22,15 @@ interface CuboxSyncSettings {
|
|||
dateFormat: string;
|
||||
lastSyncTime: number;
|
||||
lastSyncCardId: string | null;
|
||||
syncing: boolean; // 添加同步状态标志
|
||||
}
|
||||
|
||||
const DEFAULT_SETTINGS: CuboxSyncSettings = {
|
||||
domain: '', // 默认为空,表示未选择
|
||||
apiKey: '',
|
||||
folderFilter: [],
|
||||
typeFilter: '',
|
||||
statusFilter: 'all',
|
||||
typeFilter: [],
|
||||
statusFilter: ['all'],
|
||||
syncFrequency: 30, // 分钟
|
||||
targetFolder: 'Cubox',
|
||||
filenameTemplate: '{{cardTitle}}-{{createDate}}',
|
||||
|
|
@ -36,7 +39,8 @@ const DEFAULT_SETTINGS: CuboxSyncSettings = {
|
|||
highlightInContent: true,
|
||||
dateFormat: 'YYYY-MM-dd',
|
||||
lastSyncTime: 0,
|
||||
lastSyncCardId: null
|
||||
lastSyncCardId: null,
|
||||
syncing: false // 默认为非同步状态
|
||||
}
|
||||
|
||||
export default class CuboxSyncPlugin extends Plugin {
|
||||
|
|
@ -131,10 +135,27 @@ export default class CuboxSyncPlugin extends Plugin {
|
|||
}
|
||||
|
||||
async syncCubox(continueLastSync: boolean = false) {
|
||||
// 如果已经在同步中,则跳过
|
||||
if (this.settings.syncing) {
|
||||
new Notice('同步已在进行中,请等待当前同步完成');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 设置同步状态为 true
|
||||
this.settings.syncing = true;
|
||||
await this.saveSettings();
|
||||
|
||||
// 更新状态栏显示
|
||||
const statusBarItemEl = this.addStatusBarItem();
|
||||
statusBarItemEl.setText(`正在同步 Cubox...`);
|
||||
|
||||
// 检查 API 连接
|
||||
const isConnected = await this.cuboxApi.testConnection();
|
||||
if (!isConnected) {
|
||||
this.settings.syncing = false;
|
||||
await this.saveSettings();
|
||||
statusBarItemEl.setText(`上次同步: ${this.formatLastSyncTime()}`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -206,6 +227,9 @@ export default class CuboxSyncPlugin extends Plugin {
|
|||
await this.app.vault.adapter.write(filePath, finalContent);
|
||||
|
||||
syncCount++;
|
||||
|
||||
// 更新状态栏显示同步进度
|
||||
statusBarItemEl.setText(`正在同步 Cubox: ${syncCount} 篇文章已同步`);
|
||||
}
|
||||
|
||||
// 更新分页参数
|
||||
|
|
@ -225,12 +249,27 @@ export default class CuboxSyncPlugin extends Plugin {
|
|||
|
||||
// 同步完成后,清除 lastSyncCardId(表示完整同步已完成)
|
||||
this.settings.lastSyncCardId = null;
|
||||
this.settings.lastSyncTime = Date.now();
|
||||
|
||||
// 设置同步状态为 false
|
||||
this.settings.syncing = false;
|
||||
await this.saveSettings();
|
||||
|
||||
// 更新状态栏
|
||||
statusBarItemEl.setText(`上次同步: ${this.formatLastSyncTime()}`);
|
||||
|
||||
new Notice(`成功同步 ${syncCount} 篇 Cubox 文章`);
|
||||
} catch (error) {
|
||||
console.error('同步 Cubox 数据失败:', error);
|
||||
new Notice('同步 Cubox 数据失败,请检查设置和网络连接');
|
||||
|
||||
// 出错时也要重置同步状态
|
||||
this.settings.syncing = false;
|
||||
await this.saveSettings();
|
||||
|
||||
// 更新状态栏
|
||||
const statusBarItemEl = this.addStatusBarItem();
|
||||
statusBarItemEl.setText(`上次同步: ${this.formatLastSyncTime()} (失败)`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -325,6 +364,7 @@ class CuboxSyncSettingTab extends PluginSettingTab {
|
|||
.setDesc('Manage Cubox folders to be synced')
|
||||
.addButton(button => button
|
||||
.setButtonText(this.getFolderFilterButtonText())
|
||||
.setCta()
|
||||
.onClick(async () => {
|
||||
// 确保 API 已初始化
|
||||
if (!this.plugin.settings.apiKey) {
|
||||
|
|
@ -332,6 +372,9 @@ class CuboxSyncSettingTab extends PluginSettingTab {
|
|||
return;
|
||||
}
|
||||
|
||||
// 显示加载状态
|
||||
button.setButtonText('Loading folders...');
|
||||
|
||||
// 打开文件夹选择模态框
|
||||
const modal = new FolderSelectModal(
|
||||
this.app,
|
||||
|
|
@ -349,27 +392,56 @@ class CuboxSyncSettingTab extends PluginSettingTab {
|
|||
|
||||
new Setting(containerEl)
|
||||
.setName('Type Filter')
|
||||
.setDesc('Enter the xxxxx, 0 means manual sync')
|
||||
.addDropdown(dropdown => dropdown
|
||||
.addOption('', 'Sync all the items')
|
||||
.setValue(this.plugin.settings.typeFilter)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.typeFilter = value;
|
||||
await this.plugin.saveSettings();
|
||||
.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('请先设置 API Key');
|
||||
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('Only the items that match the status will be synced.')
|
||||
.addDropdown(dropdown => dropdown
|
||||
.addOption('all', 'Sync all the items')
|
||||
.addOption('read', 'Sync only read items')
|
||||
.addOption('starred', 'Sync only starred items')
|
||||
.addOption('annotated', 'Sync only annotated items')
|
||||
.setValue(this.plugin.settings.statusFilter || 'all')
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.statusFilter = value as 'all' | 'read' | 'starred' | 'annotated';
|
||||
await this.plugin.saveSettings();
|
||||
.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('请先设置 API Key');
|
||||
return;
|
||||
}
|
||||
|
||||
// 打开状态选择模态框
|
||||
const modal = new StatusSelectModal(
|
||||
this.app,
|
||||
this.plugin.settings.statusFilter,
|
||||
async (selectedStatuses) => {
|
||||
this.plugin.settings.statusFilter = selectedStatuses;
|
||||
await this.plugin.saveSettings();
|
||||
// 更新按钮文本
|
||||
button.setButtonText(this.getStatusFilterButtonText());
|
||||
}
|
||||
);
|
||||
modal.open();
|
||||
}));
|
||||
|
||||
// 同步设置部分
|
||||
|
|
@ -443,8 +515,8 @@ class CuboxSyncSettingTab extends PluginSettingTab {
|
|||
})
|
||||
// 设置文本区域的大小
|
||||
.then(textArea => {
|
||||
textArea.inputEl.rows = 6;
|
||||
textArea.inputEl.cols = 50;
|
||||
textArea.inputEl.rows = 20;
|
||||
textArea.inputEl.cols = 30;
|
||||
}))
|
||||
.addButton(button => button
|
||||
.setIcon('reset')
|
||||
|
|
@ -470,8 +542,8 @@ class CuboxSyncSettingTab extends PluginSettingTab {
|
|||
})
|
||||
// 设置文本区域的大小
|
||||
.then(textArea => {
|
||||
textArea.inputEl.rows = 8;
|
||||
textArea.inputEl.cols = 50;
|
||||
textArea.inputEl.rows = 20;
|
||||
textArea.inputEl.cols = 30;
|
||||
}))
|
||||
.addButton(button => button
|
||||
.setIcon('reset')
|
||||
|
|
@ -544,9 +616,46 @@ class CuboxSyncSettingTab extends PluginSettingTab {
|
|||
private getFolderFilterButtonText(): string {
|
||||
const count = this.plugin.settings.folderFilter.length;
|
||||
if (count === 0) {
|
||||
return 'Sync all folders';
|
||||
return 'Manage';
|
||||
} else {
|
||||
return `已选择 ${count} 个文件夹`;
|
||||
}
|
||||
}
|
||||
|
||||
// 修改获取类型过滤器按钮文本的方法
|
||||
private getTypeFilterButtonText(): string {
|
||||
const typeFilters = this.plugin.settings.typeFilter;
|
||||
if (!typeFilters || typeFilters.length === 0) {
|
||||
return 'Manage';
|
||||
} else {
|
||||
// 将类型ID转换为可读名称
|
||||
const typeMap: {[key: string]: string} = {
|
||||
'article': 'Article',
|
||||
'snippet': 'Snippet',
|
||||
'memo': 'Memo',
|
||||
'image': 'Image',
|
||||
'audio': 'Audio',
|
||||
'video': 'Video',
|
||||
'file': 'File'
|
||||
};
|
||||
|
||||
// 如果选择了所有类型,显示"All Types"
|
||||
if (typeFilters.length === Object.keys(typeMap).length) {
|
||||
return 'All Types';
|
||||
}
|
||||
|
||||
// 否则显示选择的类型数量
|
||||
return `已选择 ${typeFilters.length} 个类型`;
|
||||
}
|
||||
}
|
||||
|
||||
private getStatusFilterButtonText(): string {
|
||||
const statusFilters = this.plugin.settings.statusFilter;
|
||||
if (!statusFilters || statusFilters.length === 0 ||
|
||||
(statusFilters.length === 1 && statusFilters[0] === 'all')) {
|
||||
return 'All Items';
|
||||
} else {
|
||||
return `已选择 ${statusFilters.length} 个状态`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
130
src/modal/folderSelectModal.ts
Normal file
130
src/modal/folderSelectModal.ts
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
import { App, Modal, Setting, Notice } from 'obsidian';
|
||||
import { CuboxApi, CuboxFolder } from '../cuboxApi';
|
||||
|
||||
export class FolderSelectModal extends Modal {
|
||||
private cuboxApi: CuboxApi;
|
||||
private selectedFolders: string[];
|
||||
private onConfirm: (selectedFolders: string[]) => void;
|
||||
private folders: CuboxFolder[] = [];
|
||||
private loading: boolean = true;
|
||||
|
||||
constructor(
|
||||
app: App,
|
||||
cuboxApi: CuboxApi,
|
||||
initialSelectedFolders: string[],
|
||||
onConfirm: (selectedFolders: string[]) => void
|
||||
) {
|
||||
super(app);
|
||||
this.cuboxApi = cuboxApi;
|
||||
this.selectedFolders = [...initialSelectedFolders];
|
||||
this.onConfirm = onConfirm;
|
||||
}
|
||||
|
||||
async onOpen() {
|
||||
const { contentEl } = this;
|
||||
|
||||
// 设置标题
|
||||
contentEl.createEl('h2', { text: '选择要同步的文件夹' });
|
||||
|
||||
// 添加说明
|
||||
contentEl.createEl('p', {
|
||||
text: '选择要从 Cubox 同步的文件夹。不选择任何文件夹将同步所有内容。'
|
||||
});
|
||||
|
||||
// 添加加载指示器
|
||||
const loadingEl = contentEl.createEl('div', { text: '正在加载文件夹...' });
|
||||
|
||||
try {
|
||||
// 获取文件夹列表
|
||||
this.folders = await this.cuboxApi.getFolders();
|
||||
this.loading = false;
|
||||
|
||||
// 移除加载指示器
|
||||
loadingEl.remove();
|
||||
|
||||
// 创建文件夹列表
|
||||
const folderListEl = contentEl.createEl('div', { cls: 'folder-list' });
|
||||
|
||||
// 添加"全部"选项
|
||||
new Setting(folderListEl)
|
||||
.setName('全部文件夹')
|
||||
.setDesc('同步所有文件夹的内容')
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.selectedFolders.length === 0)
|
||||
.onChange(value => {
|
||||
if (value) {
|
||||
// 如果选择"全部",清空已选文件夹
|
||||
this.selectedFolders = [];
|
||||
// 更新所有其他切换按钮
|
||||
folderListEl.querySelectorAll('.folder-toggle').forEach((el: HTMLElement) => {
|
||||
if (el.hasClass('is-enabled')) {
|
||||
// @ts-ignore
|
||||
el.click();
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// 添加每个文件夹的选项
|
||||
this.folders.forEach(folder => {
|
||||
new Setting(folderListEl)
|
||||
.setName(folder.nested_name)
|
||||
.addToggle(toggle => {
|
||||
toggle
|
||||
.setValue(this.selectedFolders.includes(folder.id))
|
||||
.onChange(value => {
|
||||
if (value) {
|
||||
// 添加到选中列表
|
||||
this.selectedFolders.push(folder.id);
|
||||
// 如果有选择具体文件夹,取消"全部"选项
|
||||
const allToggle = folderListEl.querySelector('.setting:first-child .toggle');
|
||||
if (allToggle && allToggle.hasClass('is-enabled')) {
|
||||
// @ts-ignore
|
||||
allToggle.click();
|
||||
}
|
||||
} else {
|
||||
// 从选中列表移除
|
||||
this.selectedFolders = this.selectedFolders.filter(id => id !== folder.id);
|
||||
}
|
||||
});
|
||||
|
||||
// 添加类名以便于选择
|
||||
toggle.toggleEl.addClass('folder-toggle');
|
||||
|
||||
return toggle;
|
||||
});
|
||||
});
|
||||
|
||||
// 添加确认和取消按钮
|
||||
const buttonContainer = contentEl.createEl('div', { cls: 'button-container' });
|
||||
|
||||
buttonContainer.createEl('button', { text: '取消' })
|
||||
.addEventListener('click', () => {
|
||||
this.close();
|
||||
});
|
||||
|
||||
buttonContainer.createEl('button', { text: '确认', cls: 'mod-cta' })
|
||||
.addEventListener('click', () => {
|
||||
this.onConfirm(this.selectedFolders);
|
||||
this.close();
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取文件夹列表失败:', error);
|
||||
loadingEl.setText('获取文件夹列表失败,请检查网络连接');
|
||||
|
||||
// 添加重试按钮
|
||||
contentEl.createEl('button', { text: '重试' })
|
||||
.addEventListener('click', () => {
|
||||
this.close();
|
||||
new FolderSelectModal(this.app, this.cuboxApi, this.selectedFolders, this.onConfirm).open();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onClose() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
}
|
||||
139
src/modal/statusSelectModal.ts
Normal file
139
src/modal/statusSelectModal.ts
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
import { App, Modal, Setting, Notice } from 'obsidian';
|
||||
import { ModalStyleManager } from '../utils/modalStyles';
|
||||
|
||||
export interface ContentStatus {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export class StatusSelectModal extends Modal {
|
||||
private statuses: ContentStatus[] = [
|
||||
{ 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<string> = new Set();
|
||||
private onSave: (selectedStatuses: string[]) => void;
|
||||
private allItemsId: string = 'all';
|
||||
private listEl: HTMLElement;
|
||||
private footerEl: HTMLElement;
|
||||
|
||||
constructor(app: App, initialSelected: string[], onSave: (selectedStatuses: string[]) => void) {
|
||||
super(app);
|
||||
this.onSave = onSave;
|
||||
|
||||
// 初始化已选择的状态
|
||||
if (initialSelected && initialSelected.length > 0) {
|
||||
initialSelected.forEach(id => {
|
||||
if (id) this.selectedStatuses.add(id);
|
||||
});
|
||||
} else {
|
||||
// 如果没有初始选择,默认选中"All Items"
|
||||
this.selectedStatuses.add(this.allItemsId);
|
||||
}
|
||||
}
|
||||
|
||||
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' });
|
||||
|
||||
// 创建状态列表
|
||||
this.createStatusList();
|
||||
|
||||
// 添加底部按钮
|
||||
// 取消按钮
|
||||
const cancelButton = this.footerEl.createEl('button', { text: '取消' });
|
||||
cancelButton.addEventListener('click', () => {
|
||||
this.close();
|
||||
});
|
||||
|
||||
// 保存按钮
|
||||
const saveButton = this.footerEl.createEl('button', { text: '保存', cls: 'mod-cta' });
|
||||
saveButton.addEventListener('click', () => {
|
||||
// 返回选中的状态
|
||||
const selectedIds = Array.from(this.selectedStatuses);
|
||||
this.onSave(selectedIds);
|
||||
this.close();
|
||||
});
|
||||
|
||||
// 添加样式以固定底部按钮
|
||||
this.addStyles();
|
||||
}
|
||||
|
||||
private addStyles() {
|
||||
// 使用通用样式管理器添加样式
|
||||
ModalStyleManager.addModalStyles(
|
||||
'cubox-status-modal-styles',
|
||||
'cubox-status-select-modal',
|
||||
'status-list-container'
|
||||
);
|
||||
}
|
||||
|
||||
private createStatusList() {
|
||||
// 清除现有列表
|
||||
this.listEl.empty();
|
||||
|
||||
// 添加每个状态的选项
|
||||
this.statuses.forEach(status => {
|
||||
new Setting(this.listEl)
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.selectedStatuses.has(status.id))
|
||||
.onChange(value => {
|
||||
this.handleStatusToggle(status.id, value);
|
||||
this.redraw();
|
||||
}))
|
||||
.setName(status.name);
|
||||
});
|
||||
}
|
||||
|
||||
private handleStatusToggle(statusId: string, isSelected: boolean) {
|
||||
if (statusId === this.allItemsId) {
|
||||
if (isSelected) {
|
||||
// 如果选择了"All Items",清除其他所有选择
|
||||
this.selectedStatuses.clear();
|
||||
this.selectedStatuses.add(this.allItemsId);
|
||||
} else {
|
||||
// 如果取消了"All Items"且没有其他选择,重新选中"All Items"
|
||||
if (this.selectedStatuses.size <= 1) {
|
||||
this.selectedStatuses.clear();
|
||||
this.selectedStatuses.add(this.allItemsId);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (isSelected) {
|
||||
// 如果选择了其他选项,移除"All Items"
|
||||
this.selectedStatuses.delete(this.allItemsId);
|
||||
// 添加新选择的状态
|
||||
this.selectedStatuses.add(statusId);
|
||||
} else {
|
||||
// 移除取消选择的状态
|
||||
this.selectedStatuses.delete(statusId);
|
||||
// 如果没有任何选择,默认选中"All Items"
|
||||
if (this.selectedStatuses.size === 0) {
|
||||
this.selectedStatuses.add(this.allItemsId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private redraw() {
|
||||
this.createStatusList();
|
||||
}
|
||||
|
||||
onClose() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
|
||||
// 使用通用样式管理器移除样式
|
||||
ModalStyleManager.removeModalStyles('cubox-status-modal-styles');
|
||||
}
|
||||
}
|
||||
172
src/modal/typeSelectModal.ts
Normal file
172
src/modal/typeSelectModal.ts
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
import { App, Modal, Setting, Notice } from 'obsidian';
|
||||
import { ModalStyleManager } from '../utils/modalStyles';
|
||||
|
||||
export interface ContentType {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export class TypeSelectModal extends Modal {
|
||||
private onSave: (selectedTypes: string[]) => void;
|
||||
private selectedTypes: Set<string> = new Set();
|
||||
private listEl: HTMLElement;
|
||||
private allItemsId: string = 'all';
|
||||
|
||||
// 定义可用的类型
|
||||
private types = [
|
||||
{ id: 'article', name: 'Article' },
|
||||
{ id: 'snippet', name: 'Snippet' },
|
||||
{ id: 'memo', name: 'Memo' },
|
||||
{ id: 'image', name: 'Image' },
|
||||
{ id: 'audio', name: 'Audio' },
|
||||
{ id: 'video', name: 'Video' },
|
||||
{ id: 'file', name: 'File' }
|
||||
];
|
||||
|
||||
constructor(app: App, initialSelected: string[] = [], onSave: (selectedTypes: string[]) => void) {
|
||||
super(app);
|
||||
this.onSave = onSave;
|
||||
|
||||
console.log('TypeSelectModal constructor called with initialSelected:', initialSelected);
|
||||
|
||||
// 初始化已选择的类型
|
||||
if (initialSelected && initialSelected.length > 0) {
|
||||
// 添加初始选择的类型
|
||||
initialSelected.forEach(id => {
|
||||
if (id) this.selectedTypes.add(id);
|
||||
});
|
||||
|
||||
// 检查是否所有类型都被选中,如果是则也选中"All Items"
|
||||
const allTypesSelected = this.types.every(type =>
|
||||
initialSelected.includes(type.id)
|
||||
);
|
||||
|
||||
if (allTypesSelected) {
|
||||
this.selectedTypes.add(this.allItemsId);
|
||||
}
|
||||
} else {
|
||||
// 如果没有初始选择,默认选中"All Items"和所有类型
|
||||
this.selectedTypes.add(this.allItemsId);
|
||||
this.types.forEach(type => {
|
||||
this.selectedTypes.add(type.id);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
|
||||
// 添加标题
|
||||
contentEl.createEl('h2', { text: 'Select Content Types' });
|
||||
contentEl.addClass('cubox-type-select-modal');
|
||||
|
||||
// 创建类型列表容器
|
||||
this.listEl = contentEl.createDiv({ cls: 'type-list-container' });
|
||||
|
||||
// 创建类型列表
|
||||
this.createTypeList();
|
||||
|
||||
// 添加底部按钮容器 - 使用与 StatusSelectModal 相同的类名
|
||||
const footerEl = contentEl.createDiv({ cls: 'modal-footer' });
|
||||
|
||||
// 添加取消按钮
|
||||
const cancelButton = footerEl.createEl('button', { text: 'Cancel' });
|
||||
cancelButton.addEventListener('click', () => {
|
||||
this.close();
|
||||
});
|
||||
|
||||
// 添加保存按钮
|
||||
const saveButton = footerEl.createEl('button', { text: 'Save', cls: 'mod-cta' });
|
||||
saveButton.addEventListener('click', () => {
|
||||
// 过滤掉"All Items",只保留实际类型
|
||||
const selectedTypes = Array.from(this.selectedTypes)
|
||||
.filter(id => id !== this.allItemsId);
|
||||
|
||||
this.onSave(selectedTypes);
|
||||
this.close();
|
||||
});
|
||||
|
||||
// 添加样式
|
||||
this.addStyles();
|
||||
}
|
||||
|
||||
private addStyles() {
|
||||
// 使用通用样式管理器添加样式
|
||||
ModalStyleManager.addModalStyles(
|
||||
'cubox-modal-styles',
|
||||
'cubox-type-select-modal',
|
||||
'type-list-container'
|
||||
);
|
||||
}
|
||||
|
||||
private createTypeList() {
|
||||
// 清除现有列表
|
||||
this.listEl.empty();
|
||||
|
||||
// 添加"All Items"选项
|
||||
new Setting(this.listEl)
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.selectedTypes.has(this.allItemsId))
|
||||
.onChange(value => {
|
||||
if (value) {
|
||||
// 选中"All Items"时,选中所有类型
|
||||
this.selectedTypes.add(this.allItemsId);
|
||||
this.types.forEach(type => {
|
||||
this.selectedTypes.add(type.id);
|
||||
});
|
||||
} else {
|
||||
// 取消选中"All Items"时,取消选中所有类型
|
||||
this.selectedTypes.delete(this.allItemsId);
|
||||
this.types.forEach(type => {
|
||||
this.selectedTypes.delete(type.id);
|
||||
});
|
||||
}
|
||||
this.redraw();
|
||||
}))
|
||||
.setName('All Items');
|
||||
|
||||
// 添加每个类型的选项
|
||||
this.types.forEach(type => {
|
||||
new Setting(this.listEl)
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.selectedTypes.has(type.id))
|
||||
.onChange(value => {
|
||||
this.handleTypeToggle(type.id, value);
|
||||
this.redraw();
|
||||
}))
|
||||
.setName(type.name);
|
||||
});
|
||||
}
|
||||
|
||||
private handleTypeToggle(typeId: string, isSelected: boolean) {
|
||||
if (isSelected) {
|
||||
this.selectedTypes.add(typeId);
|
||||
|
||||
// 检查是否所有类型都被选中
|
||||
const allTypesSelected = this.types.every(type =>
|
||||
this.selectedTypes.has(type.id)
|
||||
);
|
||||
|
||||
// 不再自动选中"All Items",即使所有类型都被选中
|
||||
// 只有当用户手动选择"All Items"时才选中它
|
||||
} else {
|
||||
this.selectedTypes.delete(typeId);
|
||||
|
||||
// 如果有任何类型未被选中,取消选中"All Items"
|
||||
this.selectedTypes.delete(this.allItemsId);
|
||||
}
|
||||
}
|
||||
|
||||
private redraw() {
|
||||
this.createTypeList();
|
||||
}
|
||||
|
||||
onClose() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
|
||||
// 使用通用样式管理器移除样式
|
||||
ModalStyleManager.removeModalStyles('cubox-modal-styles');
|
||||
}
|
||||
}
|
||||
|
|
@ -24,11 +24,35 @@ export class TemplateProcessor {
|
|||
return article.title || 'Untitled';
|
||||
}
|
||||
|
||||
// 准备用于 Mustache 的数据
|
||||
const data = this.prepareTemplateData(article);
|
||||
// 使用简单的字符串替换而不是 Mustache
|
||||
let filename = template;
|
||||
|
||||
// 使用 Mustache 渲染模板
|
||||
const filename = Mustache.render(template, data);
|
||||
// 替换所有支持的模板字段
|
||||
if (article.title) {
|
||||
filename = filename.replace(/{{{card_title}}}/g, article.title);
|
||||
filename = filename.replace(/{{{article_title}}}/g, article.title);
|
||||
}
|
||||
|
||||
const createDate = article.createTime ? formatISODateTime(article.createTime, this.dateFormat) : '';
|
||||
if (createDate) {
|
||||
filename = filename.replace(/{{{date_saved}}}/g, createDate);
|
||||
}
|
||||
|
||||
const updateDate = article.updateTime ? formatISODateTime(article.updateTime, this.dateFormat) : '';
|
||||
if (updateDate) {
|
||||
filename = filename.replace(/{{{date_updated}}}/g, updateDate);
|
||||
}
|
||||
|
||||
if (article.domain) {
|
||||
filename = filename.replace(/{{{site_domain}}}/g, article.domain);
|
||||
}
|
||||
|
||||
if (article.type) {
|
||||
filename = filename.replace(/{{{type}}}/g, article.type);
|
||||
}
|
||||
|
||||
// 移除任何未被替换的模板标记 (如果某些字段不存在)
|
||||
filename = filename.replace(/{{{[^}]+}}}/g, '');
|
||||
|
||||
// 处理文件名安全性
|
||||
return generateSafeFileArticleName(filename);
|
||||
|
|
|
|||
53
src/utils/modalStyles.ts
Normal file
53
src/utils/modalStyles.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
/**
|
||||
* 模态框样式工具类,用于管理模态框的通用样式
|
||||
*/
|
||||
export class ModalStyleManager {
|
||||
/**
|
||||
* 为模态框添加通用样式
|
||||
* @param styleId 样式元素的ID
|
||||
* @param modalClass 模态框的主类名
|
||||
* @param listContainerClass 列表容器的类名
|
||||
* @returns 创建的样式元素
|
||||
*/
|
||||
public static addModalStyles(
|
||||
styleId: string,
|
||||
modalClass: string,
|
||||
listContainerClass: string
|
||||
): HTMLStyleElement {
|
||||
const styleEl = document.createElement('style');
|
||||
styleEl.id = styleId;
|
||||
styleEl.textContent = `
|
||||
.${modalClass} {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
.${listContainerClass} {
|
||||
flex-grow: 1;
|
||||
overflow-y: auto;
|
||||
padding-top: 20px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 16px;
|
||||
border-top: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
.modal-footer button {
|
||||
margin-left: 8px;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(styleEl);
|
||||
return styleEl;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除指定ID的样式元素
|
||||
* @param styleId 样式元素的ID
|
||||
*/
|
||||
public static removeModalStyles(styleId: string): void {
|
||||
const styleEl = document.getElementById(styleId);
|
||||
if (styleEl) styleEl.remove();
|
||||
}
|
||||
}
|
||||
|
|
@ -51,19 +51,19 @@ If your plugin does not need CSS, delete this file.
|
|||
|
||||
.cubox-variables-list {
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
padding-left: 15px;
|
||||
list-style-type: disc;
|
||||
}
|
||||
|
||||
.cubox-variables-list li {
|
||||
margin-bottom: 4px;
|
||||
line-height: 1.5;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.highlight-sublist {
|
||||
margin-top: 4px;
|
||||
padding-left: 20px;
|
||||
list-style-type: circle;
|
||||
padding-left: 15px;
|
||||
list-style-type: disc;
|
||||
}
|
||||
|
||||
.cubox-reference {
|
||||
|
|
|
|||
Loading…
Reference in a new issue