From d35dcfe51307c4a70f05dc35dd2386e55128914d Mon Sep 17 00:00:00 2001 From: callmer00t Date: Tue, 19 May 2026 20:21:20 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E8=87=AA=E5=AE=9A?= =?UTF-8?q?=E4=B9=89=E6=95=B0=E7=BB=84=E5=AD=97=E6=AE=B5=E9=95=BF=E5=BA=A6?= =?UTF-8?q?=E9=99=90=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为豆瓣拉取内容中数组类型的字段(actor、director、author、translator、aliases、menu 等) 新增按内容类型可配置的最大数量限制, 解决电影演员等列表过长导致笔记冗余的问题。 - 新增设置项 arrayLengthLimits, 支持按 type(电影/电视剧/书籍/音乐/游戏/笔记/戏剧/全部) + field + limit 三元组进行配置 - 在 VariableUtil.replaceSubject 中央渲染入口集中截断, 自动覆盖所有类型的所有数组字段 - 在设置面板新增 "数组限制" 页签, 提供添加/删除/编辑 UI - 同字段同时存在 type=all 和具体类型时取较小限制值 - 新增对应中英文文案 --- src/org/wanxp/constant/DefaultSettings.ts | 1 + .../setting/ArrayLengthLimitSettingsHelper.ts | 115 ++++++++++++++++++ .../wanxp/douban/setting/DoubanSettingTab.ts | 2 + .../wanxp/douban/setting/SettingsManager.ts | 32 +++++ .../douban/setting/model/ArrayLengthLimit.ts | 27 ++++ .../setting/model/DoubanPluginSetting.ts | 2 + src/org/wanxp/lang/locale/en.ts | 11 ++ src/org/wanxp/lang/locale/zh-cn.ts | 11 ++ src/org/wanxp/utils/VariableUtil.ts | 65 ++++++++++ 9 files changed, 266 insertions(+) create mode 100644 src/org/wanxp/douban/setting/ArrayLengthLimitSettingsHelper.ts create mode 100644 src/org/wanxp/douban/setting/model/ArrayLengthLimit.ts diff --git a/src/org/wanxp/constant/DefaultSettings.ts b/src/org/wanxp/constant/DefaultSettings.ts index ff0db9b..5370798 100644 --- a/src/org/wanxp/constant/DefaultSettings.ts +++ b/src/org/wanxp/constant/DefaultSettings.ts @@ -61,6 +61,7 @@ export const DEFAULT_SETTINGS: DoubanPluginSetting = { maxStar: 5, }, searchDefaultType: SupportType.all, + arrayLengthLimits: [], } diff --git a/src/org/wanxp/douban/setting/ArrayLengthLimitSettingsHelper.ts b/src/org/wanxp/douban/setting/ArrayLengthLimitSettingsHelper.ts new file mode 100644 index 0000000..3d102d5 --- /dev/null +++ b/src/org/wanxp/douban/setting/ArrayLengthLimitSettingsHelper.ts @@ -0,0 +1,115 @@ +import {ButtonComponent, DropdownComponent, Setting, TextComponent} from "obsidian"; +import SettingsManager from "./SettingsManager"; +import {i18nHelper} from "../../lang/helper"; +import {SupportType, SupportTypeMap} from "../../constant/Constsant"; +import {ArrayLengthLimit} from "./model/ArrayLengthLimit"; + +/** + * "数组字段长度限制" 设置页签构造器。 + * + * 允许用户为指定的内容类型(电影/电视剧/书籍/音乐/游戏/笔记/戏剧/全部)中的数组字段 + * (如 actor/director/author/translator/aliases/...) 自定义最大保留数量。 + */ +export function constructArrayLengthLimitSettingsUI(containerEl: HTMLElement, manager: SettingsManager) { + containerEl.createEl('p', {text: i18nHelper.getMessage('1271')}); + containerEl.createEl('p', {text: i18nHelper.getMessage('1272')}); + + if (!manager.plugin.settings.arrayLengthLimits) { + manager.plugin.settings.arrayLengthLimits = []; + } + const limits = manager.plugin.settings.arrayLengthLimits; + + new Setting(containerEl) + .setDesc(i18nHelper.getMessage('1273')) + .addButton((button) => { + button.setButtonText(i18nHelper.getMessage('124101')); + button.setTooltip(i18nHelper.getMessage('127401')); + button.setIcon('plus'); + button.onClick(async () => { + await manager.addArrayLengthLimit(); + renderItems(list, manager); + }); + }); + + const list = containerEl.createDiv('array-length-limit-list'); + renderItems(list, manager); +} + +function renderItems(containerEl: HTMLElement, manager: SettingsManager) { + containerEl.empty(); + const limits = manager.plugin.settings.arrayLengthLimits || []; + for (let i = 0; i < limits.length; i++) { + buildItem(containerEl, manager, limits, i); + } +} + +function buildItem(containerEl: HTMLElement, manager: SettingsManager, limits: ArrayLengthLimit[], idx: number) { + const data = limits[idx]; + + const item = containerEl.createEl('li'); + + // 适用类型 + item.createEl('span', {text: i18nHelper.getMessage('127402')}); + const typeDropdown = new DropdownComponent(item); + for (const fieldSelect in SupportType) { + typeDropdown.addOption(fieldSelect, i18nHelper.getMessage(fieldSelect)); + } + let dataTypeValue = data.type; + if (typeof dataTypeValue === 'string') { + // @ts-ignore + dataTypeValue = SupportTypeMap[dataTypeValue] || SupportType.all; + } + typeDropdown.setValue(dataTypeValue || SupportType.all) + .onChange(async (value: SupportType) => { + limits[idx].type = value; + await manager.plugin.saveSettings(); + }); + const typeEl = typeDropdown.selectEl; + typeEl.addClass('obsidian_douban_settings_input'); + item.appendChild(typeEl); + + // 字段名 + item.createEl('span', {text: i18nHelper.getMessage('127403')}); + const fieldField = new TextComponent(item); + fieldField.setPlaceholder(i18nHelper.getMessage('127404')) + .setValue(data.field || '') + .onChange(async (value) => { + limits[idx].field = (value || '').trim(); + await manager.plugin.saveSettings(); + }); + const fieldEl = fieldField.inputEl; + fieldEl.addClass('obsidian_douban_settings_input'); + item.appendChild(fieldEl); + + // 最大数量 + item.createEl('span', {text: i18nHelper.getMessage('127405')}); + const limitField = new TextComponent(item); + limitField.setPlaceholder(i18nHelper.getMessage('127406')) + .setValue(data.limit != null ? String(data.limit) : '') + .onChange(async (value) => { + const parsed = parseInt(value, 10); + if (Number.isNaN(parsed) || parsed < 0) { + // 无效输入忽略, 保持原值 + return; + } + limits[idx].limit = parsed; + await manager.plugin.saveSettings(); + }); + const limitEl = limitField.inputEl; + limitEl.type = 'number'; + limitEl.min = '0'; + limitEl.addClass('obsidian_douban_settings_input'); + item.appendChild(limitEl); + + // 删除按钮 + const removeBtn = new ButtonComponent(item); + removeBtn.setIcon('minus-with-circle'); + removeBtn.setTooltip(i18nHelper.getMessage('127407')); + removeBtn.onClick(async () => { + await manager.removeArrayLengthLimit(idx); + renderItems(containerEl, manager); + }); + const removeBtnEl = removeBtn.buttonEl; + removeBtnEl.addClass('obsidian_douban_settings_button'); + item.appendChild(removeBtnEl); +} diff --git a/src/org/wanxp/douban/setting/DoubanSettingTab.ts b/src/org/wanxp/douban/setting/DoubanSettingTab.ts index ff67f7a..b2c3b6d 100644 --- a/src/org/wanxp/douban/setting/DoubanSettingTab.ts +++ b/src/org/wanxp/douban/setting/DoubanSettingTab.ts @@ -11,6 +11,7 @@ import { constructAdvancedUI } from "./AdvancedSettingsHelper"; import {arraySettingDisplay, arraySettingDisplayUI} from "./ArrayDisplayTypeSettingsHelper"; import {i18nHelper} from "../../lang/helper"; import {constructLoginUI} from "./LoginSettingsHelper"; +import {constructArrayLengthLimitSettingsUI} from "./ArrayLengthLimitSettingsHelper"; /** * 部分逻辑参考以下项目 @@ -45,6 +46,7 @@ export class DoubanSettingTab extends PluginSettingTab { {name: i18nHelper.getMessage('1260'), construct: constructLoginUI}, {name: i18nHelper.getMessage('1220'), construct: constructOutUI}, {name: i18nHelper.getMessage('120601'), construct: arraySettingDisplayUI}, + {name: i18nHelper.getMessage('1270'), construct: constructArrayLengthLimitSettingsUI}, {name: i18nHelper.getMessage('1240'), construct: constructCustomPropertySettingsUI}, {name: i18nHelper.getMessage('1230'), construct: constructTemplateVariablesUI}, {name: i18nHelper.getMessage('1250'), construct: constructAdvancedUI} diff --git a/src/org/wanxp/douban/setting/SettingsManager.ts b/src/org/wanxp/douban/setting/SettingsManager.ts index 963fc04..e1a24ed 100644 --- a/src/org/wanxp/douban/setting/SettingsManager.ts +++ b/src/org/wanxp/douban/setting/SettingsManager.ts @@ -19,6 +19,7 @@ import { } from "./model/ArraySetting"; import {logger} from "bs-logger"; import {i18nHelper} from "../../lang/helper"; +import {ArrayLengthLimit} from "./model/ArrayLengthLimit"; export default class SettingsManager { app: App; @@ -247,4 +248,35 @@ export default class SettingsManager { getSettings() { return this.settings; } + + /** + * 添加一条数组长度限制配置, 默认 type=all, field 为空, limit=5。 + */ + async addArrayLengthLimit(): Promise { + if (!this.settings.arrayLengthLimits) { + this.settings.arrayLengthLimits = []; + } + const limit: ArrayLengthLimit = { + type: SupportType.all, + field: '', + limit: 5, + }; + this.settings.arrayLengthLimits.push(limit); + await this.plugin.saveSettings(); + return limit; + } + + /** + * 移除指定下标的数组长度限制配置。 + */ + async removeArrayLengthLimit(index: number): Promise { + if (!this.settings.arrayLengthLimits) { + return; + } + if (index < 0 || index >= this.settings.arrayLengthLimits.length) { + return; + } + this.settings.arrayLengthLimits.splice(index, 1); + await this.plugin.saveSettings(); + } } diff --git a/src/org/wanxp/douban/setting/model/ArrayLengthLimit.ts b/src/org/wanxp/douban/setting/model/ArrayLengthLimit.ts new file mode 100644 index 0000000..8bded9b --- /dev/null +++ b/src/org/wanxp/douban/setting/model/ArrayLengthLimit.ts @@ -0,0 +1,27 @@ +import {SupportType} from "../../../constant/Constsant"; + +/** + * 数组字段长度限制配置 + * + * 用于限制从豆瓣拉取的内容中数组类型字段(如 actor、director、author 等) + * 写入笔记时的最大元素数量。 + * + * 例如: 设置 type=movie, field=actor, limit=5, + * 则电影的演员列表只会保留前 5 个写入笔记。 + * + * 当 type=all 时, 限制将作用于所有类型中匹配 field 名称的字段。 + */ +export interface ArrayLengthLimit { + /** + * 适用的内容类型 (movie/book/music/teleplay/game/note/theater/all) + */ + type: SupportType; + /** + * 字段名 (如 actor, director, author, translator, aliases 等) + */ + field: string; + /** + * 最大保留元素数量, <=0 表示不生效 + */ + limit: number; +} diff --git a/src/org/wanxp/douban/setting/model/DoubanPluginSetting.ts b/src/org/wanxp/douban/setting/model/DoubanPluginSetting.ts index 39e1deb..007ca7e 100644 --- a/src/org/wanxp/douban/setting/model/DoubanPluginSetting.ts +++ b/src/org/wanxp/douban/setting/model/DoubanPluginSetting.ts @@ -4,6 +4,7 @@ import {ArraySetting} from "./ArraySetting"; import {ScoreSetting} from "./ScoreSetting"; import PictureBedSetting from "./PictureBedSetting"; import {SupportType} from "../../../constant/Constsant"; +import {ArrayLengthLimit} from "./ArrayLengthLimit"; export interface DoubanPluginSetting { onlineSettingsFileName: string; @@ -42,4 +43,5 @@ export interface DoubanPluginSetting { arraySettings: ArraySetting[], scoreSetting: ScoreSetting, searchDefaultType: SupportType, + arrayLengthLimits: ArrayLengthLimit[], } diff --git a/src/org/wanxp/lang/locale/en.ts b/src/org/wanxp/lang/locale/en.ts index b09801f..6d8a03c 100644 --- a/src/org/wanxp/lang/locale/en.ts +++ b/src/org/wanxp/lang/locale/en.ts @@ -138,6 +138,17 @@ PS: This file could be delete if you want to. '1220': `Output`, '1230': `Description`, '1260': `Login`, + '1270': `Array Limit`, + '1271': `Limit the maximum number of items for array fields (such as actor, director, author, translator, aliases, ...) in subjects (movie / teleplay / book / music / game / ...). Items beyond the limit will not be written to the note.`, + '1272': `Tip: When the "Apply Type" is "all", the limit applies to matching fields across all subject types. Type-specific limits take precedence when both exist for the same field.`, + '1273': `Add a limit, e.g. keep only the first 5 actors of a movie.`, + '127401': `Add a new array length limit`, + '127402': `Apply Type:`, + '127403': `Field:`, + '127404': `e.g. actor / director / author`, + '127405': `Max Count:`, + '127406': `e.g. 5`, + '127407': `Delete this limit`, '1204': `Set template file path. The template file will be used when creating new notes . If keep empty, it will use the default template file to create file. All the usable variables at tab "Description" and "Custom Variables"`, '1205': `🧡Tip: You can click the 'Copy' button to copy default template content, then create and paste to your own template file. After that, back to select the file. `, '1250': `Advanced`, diff --git a/src/org/wanxp/lang/locale/zh-cn.ts b/src/org/wanxp/lang/locale/zh-cn.ts index f3b5eed..3ea9c71 100644 --- a/src/org/wanxp/lang/locale/zh-cn.ts +++ b/src/org/wanxp/lang/locale/zh-cn.ts @@ -153,6 +153,17 @@ export default { '1220': `输出`, '1230': `字段`, '1260': `登录`, + '1270': `数组限制`, + '1271': `针对内容类型(电影/电视剧/书籍/音乐/游戏等)中的数组字段(如 actor、director、author、translator、aliases 等)设置最大保留数量, 超出部分将不会写入笔记。`, + '1272': `提示: "适用类型"选择"全部类型"时, 限制将作用于所有类型中匹配字段名的字段; 选择具体类型时仅作用于该类型, 同字段同时存在两种限制时取较小值。`, + '1273': `添加一条限制配置, 例如指定 movie 的 actor 只保留前 5 位。`, + '127401': `添加数组长度限制`, + '127402': `适用类型:`, + '127403': `字段名:`, + '127404': `如 actor / director / author 等`, + '127405': `最大数量:`, + '127406': `如 5`, + '127407': `删除该限制`, '1204': `配置对应类型的模板文件, 如果为空则使用默认模板. 模板可使用的参数在页签'字段'和'自定义字段'中查看`, '1205': `🧡提示: 建议点击右侧'复制'默认模板按钮, 然后在新建的文件中粘贴修改模板, 最后回到此处选择对应模板. `, diff --git a/src/org/wanxp/utils/VariableUtil.ts b/src/org/wanxp/utils/VariableUtil.ts index d7b350a..7edf133 100644 --- a/src/org/wanxp/utils/VariableUtil.ts +++ b/src/org/wanxp/utils/VariableUtil.ts @@ -9,6 +9,7 @@ import {DataField} from "./model/DataField"; import {FieldVariable} from "./model/FieldVariable"; import {CustomProperty} from "../douban/setting/model/CustomProperty"; import {FileUtil} from "./FileUtil"; +import {ArrayLengthLimit} from "../douban/setting/model/ArrayLengthLimit"; type TargetType = 'text' | 'path' | 'yml_text'; @@ -36,10 +37,12 @@ export class VariableUtil { return content; } if (obj instanceof Map) { + this.applyArrayLengthLimits(subjectType, obj, settingManager); this.handleCustomVariable(subjectType, obj, settingManager, 'text') content = this.replaceMap(obj, allVariables, content, settingManager, targetType); }else { const map = this.objToMap(obj); + this.applyArrayLengthLimits(subjectType, map, settingManager); this.handleCustomVariable(subjectType, map, settingManager, 'text') content = this.replaceMap(map, allVariables, content, settingManager, targetType); } @@ -277,6 +280,68 @@ export class VariableUtil { return map; } + /** + * 根据用户配置的 ArrayLengthLimit 截断 variableMap 中匹配的数组字段。 + * + * 仅对 DataField.value 为数组的字段生效。当 limit > 0 且数组长度大于 limit 时, + * 会取前 limit 个元素生成新的 DataField 替换原值; origin 保持不变以便后续逻辑。 + * + * 当 type=all 时, 该限制作用于所有内容类型, 否则仅作用于 type 与当前 subjectType 相等的条目。 + * + * @param subjectType 当前内容类型 + * @param variableMap 变量映射 + * @param settingManager + */ + static applyArrayLengthLimits(subjectType: SupportType, + variableMap: Map, + settingManager: SettingsManager): void { + if (!variableMap || variableMap.size == 0) { + return; + } + // @ts-ignore + const limits: ArrayLengthLimit[] = settingManager.getSetting('arrayLengthLimits'); + if (!limits || limits.length == 0) { + return; + } + const effectiveLimits: Map = new Map(); + limits + .filter(l => l && l.field && l.limit && l.limit > 0) + .filter(l => { + if (!l.type) { + return false; + } + const t = String(l.type).toLowerCase(); + return t === SupportType.all || t === subjectType; + }) + .forEach(l => { + const existing = effectiveLimits.get(l.field); + // 当同一字段同时存在 type=all 和 type=具体 的限制时, + // 优先使用更具体的(更小的)限制值, 行为更符合用户预期。 + if (existing == null || l.limit < existing) { + effectiveLimits.set(l.field, l.limit); + } + }); + if (effectiveLimits.size == 0) { + return; + } + effectiveLimits.forEach((limit, fieldName) => { + const dataField = variableMap.get(fieldName); + if (!dataField) { + return; + } + const value = dataField.value; + if (Array.isArray(value) && value.length > limit) { + const truncated = value.slice(0, limit); + variableMap.set(fieldName, new DataField( + dataField.name, + dataField.type, + dataField.origin, + truncated + )); + } + }); + } + private static handleText(v: string, targetType: TargetType, dataField: DataField = null): string { if (targetType === 'yml_text') { return YamlUtil.handleText(v, dataField);