Compare commits

..

13 commits

Author SHA1 Message Date
delphi
3f0f7a4ad1 docs: expand sync documentation and add Chinese README
Clarify one-way sync, deduplication, filters, and common user questions to reduce support confusion.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-25 11:32:40 +08:00
delphi
571e819ffb 1.0.7 2026-04-15 08:20:20 +08:00
delphi
ec9ce75c59 Merge branch 'develop' 2026-04-15 08:19:29 +08:00
delphi
61ad142bec feat: handle API key errors in sync process 2026-04-15 08:02:35 +08:00
delphi
202cb56275 feat: enhance type selection modal with new info icon and improve type filter handling 2026-04-14 16:29:24 +08:00
delphi
618137dfca refactor: simplify modal settings with toggle components for folder, status, tag, and type selections 2026-04-14 09:11:21 +08:00
delphi
41011b56d2 chore: update Node.js version to 22.x and adjust dependencies 2026-04-14 08:21:00 +08:00
delphi
b28920e0a4 Merge branch 'develop' 2025-05-26 17:21:27 +08:00
delphi
49bce01132 Merge branch 'develop' 2025-04-12 08:42:49 +08:00
delphi
87dac77cbc Merge branch 'develop' 2025-04-07 20:32:05 +08:00
delphi
0ee78f128e Merge branch 'develop' 2025-04-03 10:06:55 +08:00
delphi
d84129617d Merge branch 'develop'
# Conflicts:
#	manifest.json
#	package-lock.json
#	package.json
#	versions.json
2025-03-19 13:27:30 +08:00
delphi
5f3d03a717 1.0.1 2025-03-19 10:44:49 +08:00
15 changed files with 574 additions and 307 deletions

View file

@ -18,7 +18,7 @@ jobs:
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: "18.x"
node-version: "22.x"
cache: 'npm'
- name: Build plugin

146
README.md
View file

@ -1,22 +1,25 @@
# Cubox Plugin for Obsidian
The official Cubox plugin for Obsidian allows you to synchronize articles and annotations from Cubox to Obsidian.
[中文文档](README.zh-CN.md)
The official Cubox plugin for Obsidian syncs articles and annotations from Cubox into your vault.
## Features
- 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
- **One-way sync**: Cubox → Obsidian only. Changes in Obsidian are not sent back to Cubox.
- **Automatic or manual sync**: Run on a schedule or trigger sync from the ribbon icon / command palette.
- **Filtering**: Filter by folder, type, tags, and status.
- **Templates**: Customize file names, front matter, and note body.
- **Annotation sync**: Export highlights and notes from Cubox into Obsidian.
## Installation
### 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
2. Navigate to the **Community plugins** tab
3. Click **Browse** and search for **Cubox**
4. Click **Install**, then **Enable**
### Manual Installation
@ -27,16 +30,125 @@ The official Cubox plugin for Obsidian allows you to synchronize articles and an
## Configuration
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)
1. **Cubox server domain**: Select the domain you use (`cubox.cc` or `cubox.pro`)
2. **Cubox API key**: Enter your API key or paste the full key link from Cubox web settings under **Extensions & Automation → API Extension**
## Usage
## Quick Start
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. Select the correct server domain and enter your API key.
2. Configure **all four filters** before the first sync:
- Folder filter
- Tag filter
- Type filter
- Status filter
3. Choose a target folder and review the default templates if needed.
4. Trigger sync manually (ribbon icon or **Sync now** command), or set an auto-sync interval.
> **Important:** Sync will not run until every filter category has at least one option selected. The default empty filter state is intentional—you must choose what to sync.
## How Sync Works
Understanding these rules prevents most confusion:
### One-way, create-only
- Sync direction is **Cubox → Obsidian only**.
- The plugin **creates new notes**. It does **not update** notes that already exist in your vault.
- If you add new highlights, edit a title, or change tags in Cubox after a note was synced, those changes **will not appear** in the existing Obsidian note.
### Each Cubox card is synced once
- Every synced note stores the Cubox card `id` in front matter (always included for deduplication).
- On later syncs, if a note with the same `id` already exists at the target path, that card is **skipped**.
- The completion notice may show `X new items, Y skipped`. **Skipped** means the card was already synced—not an error.
### Incremental fetching
- The plugin remembers where the last sync stopped and only fetches **new cards** on subsequent runs.
- Changing the **target folder** resets that cursor, so historical cards may be fetched again—but they are still subject to the one-time dedup rule above. Existing notes in the old folder are **not** updated.
### Annotations are a snapshot
- Highlights exported to Obsidian reflect what Cubox returned **at sync time**.
- Sync again after you finish annotating in Cubox. A longer interval or manual sync helps avoid exporting incomplete highlights.
### Default template does not include full article text
- The default content template includes title, description, links, and highlights.
- To sync the full article body, add `{{content}}` or `{{content_highlighted}}` to your **Content template** in settings.
## Filters
Only items that match **all** filter categories are synced:
| Filter | What it does |
|--------|----------------|
| **Folder** | Limit to selected Cubox folders, or choose **All items** |
| **Tag** | Limit to specific tags, **No tags**, or **All items** |
| **Type** | Article, Snippet, and/or **Others** (Memo, Image, Audio, Video, File) |
| **Status** | **All items**, or restrict to read / starred / annotated items |
Tips:
- If many items seem missing, review every filter—not just folders.
- Selecting multiple status options (e.g. read **and** starred) narrows results further.
- If you only need articles with highlights, use the annotated status filter—but remember that already-synced notes still will not receive new highlights later.
## Templates
Use plugin settings to customize:
- **File name template** — Mustache variables such as `{{{title}}}` and `{{{create_time}}}`
- **Metadata variables** — Front matter fields (the Cubox `id` is always written)
- **Content template** — Note body layout, including `{{#highlights}}` blocks
- **Date format** — Format used for date variables in templates
See the reference links on the settings page for full variable lists and examples.
## FAQ
### Why don't my new highlights show up in Obsidian?
The plugin does not update existing notes. New or edited highlights in Cubox stay in Cubox unless you edit the Obsidian note yourself.
There is no built-in "re-sync and overwrite" option. Workarounds:
- Manually update the note in Obsidian, or
- Delete the Obsidian note and sync again (only if you understand dedup and filter behavior)
### Can I sync only once, or does it keep syncing?
Both are possible:
- **Auto sync** runs on your chosen interval and picks up **new** Cubox cards.
- **Manual sync** (ribbon icon or command palette → **Sync now**) does the same on demand.
- Already-synced cards are skipped; the plugin is not limited to a single run.
### Why is nothing syncing?
Check these in order:
1. API key and server domain are correct
2. **All four filters** are configured (sync is blocked if any filter is empty)
3. Your Cubox items actually match the combined filter rules
### Why are many items missing?
Common causes:
1. **Filters too strict** — e.g. only annotated + specific tag + one folder
2. **Type filter****Others** not selected, so memos/files are excluded
3. **Tag filter** — only certain tags selected, so untagged items are excluded
4. **Default template** — no `{{content}}`, so notes look shorter than expected (metadata + highlights only)
5. **Synced too early** — highlights were still being edited in Cubox when the card was first exported
### What does "skipped" mean in the sync notice?
Those Cubox cards were already exported to Obsidian (matched by `id`). They were not synced again.
### Do Obsidian edits sync back to Cubox?
No. Sync is one-way: Cubox → Obsidian.
## Dependencies
@ -45,4 +157,4 @@ The official Cubox plugin for Obsidian allows you to synchronize articles and an
## License
This project is licensed under the [MIT License](https://choosealicense.com/licenses/mit/).
This project is licensed under the [MIT License](https://choosealicense.com/licenses/mit/).

160
README.zh-CN.md Normal file
View file

@ -0,0 +1,160 @@
# Cubox Obsidian 插件
[English](README.md)
Cubox 官方 Obsidian 插件,可将 Cubox 中的收藏与标注单向同步到 Obsidian 库中。
## 功能
- **单向同步**:仅 Cubox → ObsidianObsidian 中的修改不会回写到 Cubox
- **自动或手动同步**:按间隔自动执行,或通过侧边栏图标 / 命令面板手动触发
- **筛选**:按文件夹、类型、标签、状态过滤要同步的内容
- **模板**自定义文件名、元数据front matter与正文格式
- **标注同步**:将 Cubox 中的高亮与笔记导出到 Obsidian
## 安装
### 从社区插件安装
1. 打开 Obsidian **设置**
2. 进入 **社区插件**
3. 点击 **浏览**,搜索 **Cubox**
4. 点击 **安装**,然后 **启用**
### 手动安装
1. 下载最新的 `main.js`、`manifest.json` 和 `styles.css`
2. 在库中创建 `.obsidian/plugins/obsidian-cubox` 文件夹
3. 将上述文件复制到该文件夹
4. 在 Obsidian 设置中启用插件
## 配置
1. **Cubox 服务器域名**:选择你使用的域名(`cubox.cc` 或 `cubox.pro`
2. **Cubox API Key**:填写 API Key或粘贴 Cubox 网页端 **扩展与自动化 → API 扩展** 中的完整链接
## 快速开始
1. 选择正确的服务器域名并填写 API Key。
2. **首次同步前,必须配置全部四项筛选**
- 文件夹筛选Folder filter
- 标签筛选Tag filter
- 类型筛选Type filter
- 状态筛选Status filter
3. 设置目标文件夹,按需调整默认模板。
4. 手动同步(侧边栏图标或 **Sync now** 命令),或设置自动同步间隔。
> **重要:** 任一筛选项未配置时,同步不会执行。默认四项均为空是正常状态,需要你主动选择要同步的范围。
## 同步机制说明
理解以下规则,可以避免大多数使用困惑:
### 单向、只创建不更新
- 同步方向为 **Cubox → Obsidian**
- 插件只会 **创建新笔记****不会更新** 库中已存在的笔记。
- 某条收藏同步到 Obsidian 之后,你在 Cubox 里新增标注、修改标题或标签,**不会** 自动反映到已有 Obsidian 笔记中。
### 每条 Cubox 收藏只同步一次
- 每条同步的笔记会在 front matter 中写入 Cubox 卡片 `id`(用于去重,始终会写入)。
- 后续同步时,若目标路径下已存在相同 `id` 的笔记,该卡片会被 **跳过**
- 完成提示可能显示 `X new items, Y skipped`。**skipped** 表示该卡片此前已同步,**不是错误**。
### 增量拉取
- 插件会记录上次同步进度,后续运行主要拉取 **新的** Cubox 卡片。
- 修改 **目标文件夹** 会重置同步游标,可能再次尝试拉取历史卡片——但仍受「每条只同步一次」规则约束;**旧文件夹里的笔记不会被更新**。
### 标注是同步时刻的快照
- 导出到 Obsidian 的标注,是同步时 Cubox 返回的数据。
- 建议在 Cubox 中完成标注后再同步;使用较长间隔或手动同步,可减少「标注尚未写完就被导出」的情况。
### 默认模板不含文章正文
- 默认正文模板包含标题、摘要、链接和标注列表。
- 若需要同步 **全文正文**,请在设置的 **Content template** 中加入 `{{content}}``{{content_highlighted}}`
## 筛选说明
只有 **同时满足** 各项筛选条件的内容才会被同步:
| 筛选项 | 说明 |
|--------|------|
| **文件夹** | 指定 Cubox 文件夹,或选择 **All items**(全部) |
| **标签** | 指定标签、**No tags**(无标签),或 **All items** |
| **类型** | Article、Snippet以及 **Others**Memo、Image、Audio、Video、File |
| **状态** | **All items**,或限定已读 / 星标 / 有标注 |
提示:
- 若感觉「少同步了很多」,请逐项检查四项筛选,而不只是文件夹。
- 同时勾选多种状态(例如已读 **且** 星标)会进一步缩小范围。
- 若只要带标注的内容,可使用 annotated 状态筛选——但已同步过的笔记仍不会收到 Cubox 里新增的标注。
## 模板
可在插件设置中自定义:
- **File name template** — 文件名,支持 Mustache 变量,如 `{{{title}}}`、`{{{create_time}}}`
- **Metadata variables** — front matter 字段Cubox `id` 始终会写入)
- **Content template** — 正文布局,含 `{{#highlights}}` 等块
- **Date format** — 模板中日期的显示格式
设置页中的 reference 链接可查看完整变量说明与示例。
## 常见问题
### 为什么在 Cubox 里新加的标注Obsidian 里没有?
插件 **不会更新** 已存在的笔记。Cubox 中新增或修改的标注,不会自动写入 Obsidian需要你在 Obsidian 中手动编辑。
目前没有内置的「重新同步并覆盖」功能。可选做法:
- 在 Obsidian 中手动更新笔记,或
- 删除 Obsidian 中的对应笔记后再次同步(请先理解去重与筛选规则)
### 是不是只能同步一次?还能继续同步吗?
可以持续同步:
- **自动同步** 按你设置的间隔运行,拉取 **新** 的 Cubox 卡片。
- **手动同步**(侧边栏图标或命令面板 → **Sync now**)随时触发,行为相同。
- 已同步过的卡片会被跳过;插件 **并非** 只能运行一次。
### 为什么完全没有任何内容被同步?
请按顺序检查:
1. API Key 与服务器域名是否正确
2. **四项筛选是否都已配置**(任一项为空则不会同步)
3. Cubox 中的内容是否确实满足全部筛选条件
### 为什么感觉少同步了很多数据?
常见原因:
1. **筛选过严** — 例如同时限定「有标注 + 某标签 + 某文件夹」
2. **类型筛选** — 未勾选 **Others**,备忘录、图片、文件等类型不会被同步
3. **标签筛选** — 只选了部分标签,无标签条目会被排除
4. **默认模板** — 未包含 `{{content}}`,笔记看起来比预期短(仅有元数据与标注,无全文)
5. **同步过早** — 卡片首次同步时 Cubox 中的标注尚未写完
### 同步完成提示里的 "skipped" 是什么意思?
表示这些 Cubox 卡片此前已导出到 Obsidian通过 `id` 匹配),本次被跳过,未再次写入。
### Obsidian 里的修改会同步回 Cubox 吗?
不会。同步是单向的:**Cubox → Obsidian**。
## 依赖
- [Mustache](https://mustache.github.io/):模板渲染
- [Luxon](https://moment.github.io/luxon/#/formatting?id=table-of-tokens):日期时间处理
## 许可证
本项目采用 [MIT License](https://choosealicense.com/licenses/mit/)。

View file

@ -1,7 +1,7 @@
{
"id": "cubox-sync",
"name": "Cubox",
"version": "1.0.6",
"version": "1.0.7",
"minAppVersion": "0.15.0",
"description": "Sync your Cubox articles & annotations",
"author": "delphi-2015",

28
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "obsidian-cubox",
"version": "1.0.6",
"version": "1.0.7",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "obsidian-cubox",
"version": "1.0.6",
"version": "1.0.7",
"license": "MIT",
"dependencies": {
"luxon": "^3.1.1",
@ -16,7 +16,7 @@
"@types/jest": "^29.5.0",
"@types/luxon": "^3.4.2",
"@types/mustache": "^4.2.5",
"@types/node": "^16.11.6",
"@types/node": "^22.15.0",
"@typescript-eslint/eslint-plugin": "5.29.0",
"@typescript-eslint/parser": "5.29.0",
"builtin-modules": "3.3.0",
@ -29,6 +29,9 @@
"ts-jest": "^29.1.0",
"tslib": "2.4.0",
"typescript": "4.7.4"
},
"engines": {
"node": ">=22"
}
},
"node_modules/@ampproject/remapping": {
@ -1636,10 +1639,14 @@
"dev": true
},
"node_modules/@types/node": {
"version": "16.18.126",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.126.tgz",
"integrity": "sha512-OTcgaiwfGFBKacvfwuHzzn1KLxH/er8mluiy8/uM3sGXHaRe73RrSIj01jow9t4kJEW633Ov+cOexXeiApTyAw==",
"dev": true
"version": "22.19.17",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz",
"integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/@types/stack-utils": {
"version": "2.0.3",
@ -5766,6 +5773,13 @@
"node": ">=4.2.0"
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
},
"node_modules/universalify": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",

View file

@ -1,6 +1,6 @@
{
"name": "obsidian-cubox",
"version": "1.0.6",
"version": "1.0.7",
"description": "This is a cubox plugin for Obsidian (https://obsidian.md)",
"main": "main.js",
"scripts": {
@ -13,9 +13,12 @@
"keywords": [],
"author": "",
"license": "MIT",
"engines": {
"node": ">=22"
},
"devDependencies": {
"@types/mustache": "^4.2.5",
"@types/node": "^16.11.6",
"@types/node": "^22.15.0",
"@types/luxon": "^3.4.2",
"@typescript-eslint/eslint-plugin": "5.29.0",
"@typescript-eslint/parser": "5.29.0",

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;
@ -50,6 +51,13 @@ interface ListResponse {
data: CuboxArticle[];
}
export class CuboxApiKeyMissingError extends Error {
constructor(message = 'Cubox API key is missing or invalid.') {
super(message);
this.name = 'CuboxApiKeyMissingError';
}
}
interface ContentResponse {
code: number;
message: string;
@ -152,9 +160,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);
}
// 添加状态过滤
@ -182,6 +190,10 @@ export class CuboxApi {
method: 'POST',
body: JSON.stringify(requestBody)
}) as ListResponse;
if (response.code === -1100) {
throw new CuboxApiKeyMissingError(response.message || 'API key not found');
}
const articles = response.data ?? [];
const hasMore = articles && articles.length >= pageSize;

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();
}));
@ -489,7 +490,7 @@ export class CuboxSyncSettingTab extends PluginSettingTab {
textComponent.inputEl.disabled = true;
} else {
// 已选择域名,更新描述和链接
const url = `https://${domain}/my/settings/extensions`;
const url = `https://${domain}/web/settings/extensions`;
const descEl = this.apiKeySetting.descEl;
// 清空现有描述
@ -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

@ -1,8 +1,9 @@
import { addIcon, Notice, Plugin, TFile, TFolder } from 'obsidian';
import { CuboxApi } from './cuboxApi';
import { CuboxApi, CuboxApiKeyMissingError } 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() {
@ -209,6 +211,10 @@ export default class CuboxSyncPlugin extends Plugin {
new Notice(message);
} catch (error) {
console.error('同步 Cubox 数据失败:', error);
if (error instanceof CuboxApiKeyMissingError) {
new Notice('Cubox API Key not found. Please set your API Key in plugin settings and try again.');
return;
}
new Notice('Cubox sync failed. Please check settings or network.');
} finally {
this.settings.syncing = false;

View file

@ -68,48 +68,36 @@ export class FolderSelectModal extends Modal {
private createFolderList() {
this.listEl.empty();
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();
});
// 添加每个文件夹的选项
this.folders.forEach(folder => {
const folderSetting = new Setting(this.listEl)
.setName(folder.nested_name);
// 如果"All Items"被选中,禁用其他选项
if (this.selectedFolders.has(ALL_FOLDERS_ID)) {
folderSetting.settingEl.addClass('is-disabled');
} else {
// 添加选中状态的类
if (this.selectedFolders.has(folder.id)) {
folderSetting.settingEl.addClass('is-selected');
}
// 添加点击事件
folderSetting.settingEl.addEventListener('click', () => {
const isCurrentlySelected = this.selectedFolders.has(folder.id);
this.handleFolderToggle(folder.id, !isCurrentlySelected);
new Setting(this.listEl)
.setName('All items')
.addToggle(toggle => {
toggle.setValue(this.selectedFolders.has(ALL_FOLDERS_ID));
toggle.onChange(value => {
this.handleFolderToggle(ALL_FOLDERS_ID, value);
this.redraw();
});
}
});
this.folders.forEach(folder => {
const allOn = this.selectedFolders.has(ALL_FOLDERS_ID);
const folderSetting = new Setting(this.listEl)
.setName(folder.nested_name)
.addToggle(toggle => {
toggle.setDisabled(allOn);
toggle.setValue(!allOn && this.selectedFolders.has(folder.id));
if (!allOn) {
toggle.onChange(value => {
this.handleFolderToggle(folder.id, value);
this.redraw();
});
}
});
if (allOn) {
folderSetting.settingEl.addClass('is-disabled');
}
});
}
private isFolderSelected(folderId: string): boolean {
return this.selectedFolders.has(folderId);
}
private handleFolderToggle(folderId: string, isSelected: boolean) {
if (folderId === ALL_FOLDERS_ID) {
if (isSelected) {

View file

@ -85,36 +85,29 @@ export class StatusSelectModal extends Modal {
// 添加每个状态的选项
this.statuses.forEach(status => {
const allOn = this.selectedStatuses.has(ALL_STATUS_ID);
const statusSetting = new Setting(this.listEl)
.setName(status.name);
// 如果是"All Items"选项
if (status.id === ALL_STATUS_ID) {
// 添加选中状态的类
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);
this.redraw();
});
} else {
// 如果"All Items"被选中,禁用其他选项
if (this.selectedStatuses.has(ALL_STATUS_ID)) {
statusSetting.settingEl.addClass('is-disabled');
} else {
if (this.selectedStatuses.has(status.id)) {
statusSetting.settingEl.addClass('is-selected');
.setName(status.name)
.addToggle(toggle => {
if (status.id === ALL_STATUS_ID) {
toggle.setValue(this.selectedStatuses.has(ALL_STATUS_ID));
toggle.onChange(value => {
this.handleStatusToggle(ALL_STATUS_ID, value);
this.redraw();
});
} else {
toggle.setDisabled(allOn);
toggle.setValue(!allOn && this.selectedStatuses.has(status.id));
if (!allOn) {
toggle.onChange(value => {
this.handleStatusToggle(status.id, value);
this.redraw();
});
}
}
statusSetting.settingEl.addEventListener('click', () => {
const isCurrentlySelected = this.selectedStatuses.has(status.id);
this.handleStatusToggle(status.id, !isCurrentlySelected);
this.redraw();
});
}
});
if (status.id !== ALL_STATUS_ID && allOn) {
statusSetting.settingEl.addClass('is-disabled');
}
});
}

View file

@ -66,69 +66,52 @@ 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');
}
// 添加点击事件
allItemsSetting.settingEl.addEventListener('click', () => {
const isCurrentlySelected = this.selectedTags.has(ALL_ITEMS);
this.handleTagToggle(ALL_ITEMS, !isCurrentlySelected);
this.redraw();
});
// 添加"No Tags"选项
const noTagsSetting = new Setting(this.listEl)
.setName('No tags');
// 如果"All Items"被选中,禁用其他选项
if (this.selectedTags.has(ALL_ITEMS)) {
noTagsSetting.settingEl.addClass('is-disabled');
} else {
// 添加选中状态的类
if (this.selectedTags.has(NO_TAGS_ID)) {
noTagsSetting.settingEl.addClass('is-selected');
}
noTagsSetting.settingEl.addEventListener('click', () => {
const isCurrentlySelected = this.selectedTags.has(NO_TAGS_ID);
this.handleTagToggle(NO_TAGS_ID, !isCurrentlySelected);
this.redraw();
});
}
// 添加每个标签的选项
this.tags.forEach(tag => {
const tagSetting = new Setting(this.listEl)
.setName(tag.nested_name);
// 如果"All Items"被选中,禁用其他选项
if (this.selectedTags.has(ALL_ITEMS)) {
tagSetting.settingEl.addClass('is-disabled');
} else {
// 添加选中状态的类
if (this.selectedTags.has(tag.id)) {
tagSetting.settingEl.addClass('is-selected');
}
tagSetting.settingEl.addEventListener('click', () => {
const isCurrentlySelected = this.selectedTags.has(tag.id);
this.handleTagToggle(tag.id, !isCurrentlySelected);
new Setting(this.listEl)
.setName('All items')
.addToggle(toggle => {
toggle.setValue(this.selectedTags.has(ALL_ITEMS));
toggle.onChange(value => {
this.handleTagToggle(ALL_ITEMS, value);
this.redraw();
});
});
const allOn = this.selectedTags.has(ALL_ITEMS);
const noTagsSetting = new Setting(this.listEl)
.setName('No tags')
.addToggle(toggle => {
toggle.setDisabled(allOn);
toggle.setValue(!allOn && this.selectedTags.has(NO_TAGS_ID));
if (!allOn) {
toggle.onChange(value => {
this.handleTagToggle(NO_TAGS_ID, value);
this.redraw();
});
}
});
if (allOn) {
noTagsSetting.settingEl.addClass('is-disabled');
}
this.tags.forEach(tag => {
const tagSetting = new Setting(this.listEl)
.setName(tag.nested_name)
.addToggle(toggle => {
toggle.setDisabled(allOn);
toggle.setValue(!allOn && this.selectedTags.has(tag.id));
if (!allOn) {
toggle.onChange(value => {
this.handleTagToggle(tag.id, value);
this.redraw();
});
}
});
if (allOn) {
tagSetting.settingEl.addClass('is-disabled');
}
});
}
private isTagSelected(tagId: string): boolean {
return this.selectedTags.has(tagId);
}
private handleTagToggle(tagId: string, isSelected: boolean) {
if (tagId === ALL_ITEMS) {
if (isSelected) {

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,22 +124,38 @@ export class TypeSelectModal extends Modal {
private createTypeList() {
this.listEl.empty();
// 添加每个类型的选项
ALL_CONTENT_TYPES.forEach(typeId => {
const typeSetting = new Setting(this.listEl)
.setName(typeId);
// 添加选中状态的类
if (this.selectedTypes.has(typeId)) {
typeSetting.settingEl.addClass('is-selected');
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);
}
// 添加点击事件
typeSetting.settingEl.addEventListener('click', () => {
const isCurrentlySelected = this.selectedTypes.has(typeId);
this.handleTypeToggle(typeId, !isCurrentlySelected);
this.redraw();
row.addToggle((toggle) => {
toggle.setValue(this.selectedTypes.has(typeId));
toggle.onChange((value) => {
this.handleTypeToggle(typeId, value);
this.redraw();
});
});
});
}
@ -99,4 +176,4 @@ export class TypeSelectModal extends Modal {
const { contentEl } = this;
contentEl.empty();
}
}
}

View file

@ -78,136 +78,53 @@ If your plugin does not need CSS, delete this file.
overflow-y: auto;
padding-top: 20px;
padding-right: 10px;
padding-bottom: 12px;
overflow-anchor: none;
scroll-behavior: auto;
height: 100%;
max-height: 65vh;
}
/* Setting item styles for modal dialogs */
/* Modal lists use Obsidian Setting + ToggleComponent; keep light row chrome only */
.cubox-list-container .setting-item {
position: relative;
padding-left: 36px;
border-radius: 4px;
transition: background-color 0.1s ease;
display: flex;
align-items: center;
min-height: 40px;
box-sizing: border-box;
/* Extra vertical room so text/toggle dont sit flush on the row dividers */
padding-top: 10px;
padding-bottom: 12px;
}
.cubox-list-container .setting-item:hover {
background-color: var(--background-modifier-hover);
}
/* Setting item name styles */
.cubox-list-container .setting-item-name {
display: flex;
align-items: center;
padding: 0;
margin: 0;
}
/* Custom checkbox styles */
.cubox-list-container .setting-item::before {
content: "";
position: absolute;
width: 16px;
height: 16px;
border: 1px solid var(--checkbox-border-color, var(--background-modifier-border));
border-radius: 3px;
background-color: var(--checkbox-color, var(--background-primary));
box-sizing: border-box;
}
/* First child checkbox positioning */
.cubox-list-container .setting-item:first-child::before {
left: 10px;
top: 12px;
}
/* Other items checkbox positioning */
.cubox-list-container .setting-item:not(:first-child)::before {
left: 10px;
top: 50%;
transform: translateY(-50%);
}
/* Selected checkbox state */
.cubox-list-container .setting-item.is-selected::before {
background-color: var(--checkbox-color-checked, var(--interactive-accent));
border-color: var(--checkbox-border-color-checked, var(--interactive-accent));
}
/* Checkmark for selected items */
.cubox-list-container .setting-item.is-selected::after {
content: "";
position: absolute;
width: 5px;
height: 9px;
border-right: 2px solid var(--text-on-accent);
border-bottom: 2px solid var(--text-on-accent);
}
/* First child checkmark positioning */
.cubox-list-container .setting-item:first-child.is-selected::after {
left: 14px;
top: 13px;
transform: rotate(45deg);
}
/* Other items checkmark positioning */
.cubox-list-container .setting-item:not(:first-child).is-selected::after {
left: 14px;
top: 50%;
transform: translateY(-60%) rotate(45deg);
}
/* First item special styling */
.cubox-list-container .setting-item:first-child {
margin-top: 0;
padding-top: 0;
.cubox-list-container .setting-item.is-disabled {
opacity: 0.45;
}
.cubox-list-container .setting-item:first-child .setting-item-name {
margin-top: 0;
padding-top: 0;
position: relative;
top: 6px;
font-weight: bold;
font-weight: 600;
}
/* Disabled state styling */
.cubox-list-container .setting-item.is-disabled {
opacity: 0.3;
pointer-events: none;
}
.cubox-list-container .setting-item.is-disabled::before {
background-color: var(--background-primary);
border-color: var(--background-modifier-border);
}
.cubox-list-container .setting-item.is-disabled::after {
content: "";
position: absolute;
width: 8px;
height: 2px;
background-color: var(--text-muted);
left: 14px;
top: 50%;
transform: translateY(-50%);
border: none;
}
/* Consistent line height for all items */
.cubox-list-container .setting-item-heading,
.cubox-list-container .setting-item-name {
line-height: normal;
margin: auto 0;
}
/* Override first row bold font for type modal */
.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;
}

View file

@ -5,5 +5,6 @@
"1.0.3": "0.15.0",
"1.0.4": "0.15.0",
"1.0.5": "0.15.0",
"1.0.6": "0.15.0"
"1.0.6": "0.15.0",
"1.0.7": "0.15.0"
}