var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); // src/main.ts var obsidian = __toESM(require("obsidian")); var VIEW_TYPE = "smart-folder-view"; var FILTER_STORAGE_PREFIX = "smart-folder-view-filter:"; var FILTER_PRESET_STORAGE_PREFIX = "smart-folder-view-presets:"; var MANUAL_ORDER_STORAGE_PREFIX = "smart-folder-view-manual-order:"; var EMPTY_OPTION_VALUE = "__SFV_EMPTY__"; var DEFAULT_COLOR_CONFIG = { saturationBase: 55, saturationRange: 20, lightnessBase: 44, lightnessRange: 14, fallbackColor: "var(--interactive-accent)" }; var I18N = { zh: { title: "Smart Folder View", openBuilder: "\u65B0\u5EFA Smart Folder \u9875\u9762", openManager: "\u7BA1\u7406\u5DF2\u4FDD\u5B58\u9875\u9762", openLast: "\u6253\u5F00\u4E0A\u6B21\u9875\u9762", noProfile: "\u8FD8\u6CA1\u6709\u4FDD\u5B58\u4EFB\u4F55\u9875\u9762\uFF0C\u8BF7\u5148\u65B0\u5EFA\u3002", setupTitle: "\u914D\u7F6E\u9875\u9762", setupDesc: "\u9875\u9762\u8BBE\u7F6E\u7528\u4E8E\u951A\u5B9A\u6570\u636E\u6765\u6E90\uFF1A\u6587\u4EF6\u5939\u3001\u6A21\u677F\u6587\u4EF6\u3001\u53EF\u7B5B\u9009\u5C5E\u6027\u3002\u8BBE\u7F6E\u540E\u5148\u9884\u89C8\uFF0C\u518D\u4FDD\u5B58\u3002", profileName: "\u9875\u9762\u540D\u79F0", sourceFolder: "\u6765\u6E90\u6587\u4EF6\u5939", sourceFolderDesc: "\u4ECE\u8BE5\u6587\u4EF6\u5939\u8BFB\u53D6\u5361\u7247\u6570\u636E", templateFile: "\u6A21\u677F\u6587\u4EF6", templateFileDesc: "\u4ECE\u5168\u5E93\u9009\u62E9\u6A21\u677F\u6587\u4EF6\u8BFB\u53D6\u5143\u6570\u636E\u5B57\u6BB5\uFF08\u5E76\u878D\u5408\u8BE5\u6587\u4EF6\u5939\u5171\u7528\u5B57\u6BB5\uFF09", colorField: "\u989C\u8272\u5B57\u6BB5", sortField: "\u6392\u5E8F\u5B57\u6BB5", displayFields: "\u5C55\u793A\u5B57\u6BB5", sortOrder: "\u6392\u5E8F\u65B9\u5411", viewMode: "\u89C6\u56FE\u6A21\u5F0F", boardField: "\u770B\u677F\u5206\u7EC4\u5B57\u6BB5", preview: "\u9884\u89C8", saveAndOpen: "\u4FDD\u5B58\u5E76\u6253\u5F00", choose: "(\u8BF7\u9009\u62E9)", rootFolder: "(\u6839\u6587\u4EF6\u5939)", timeline: "\u65F6\u95F4\u8F74", board: "\u770B\u677F", asc: "\u5347\u5E8F\uFF08\u65E7\u2192\u65B0\uFF09", desc: "\u5012\u5E8F\uFF08\u65B0\u2192\u65E7\uFF09", savePage: "\u4FDD\u5B58\u9875\u9762", updatePage: "\u66F4\u65B0\u9875\u9762", openBuilderBtn: "\u65B0\u5EFA\u9875\u9762", openManagerBtn: "\u9875\u9762\u7BA1\u7406", startActionTitle: "Smart Folder", startActionDesc: "\u8BF7\u9009\u62E9\u64CD\u4F5C\uFF1A", startOpenPage: "\u6253\u5F00\u9875\u9762", startNewPage: "\u65B0\u5EFA\u9875\u9762", pageSaved: "\u9875\u9762\u5DF2\u4FDD\u5B58", pageUpdated: "\u9875\u9762\u5DF2\u66F4\u65B0", saveFailed: "\u9875\u9762\u4FDD\u5B58\u5931\u8D25", pageNamePrompt: "\u8BF7\u8F93\u5165\u9875\u9762\u540D\u79F0", managerTitle: "\u5DF2\u4FDD\u5B58\u9875\u9762", open: "\u6253\u5F00", edit: "\u7F16\u8F91", delete: "\u5220\u9664", deleteConfirm: "\u786E\u8BA4\u5220\u9664\u9875\u9762\uFF1A{{name}}\uFF1F", noFiles: '\u6587\u4EF6\u5939 "{{folder}}" \u91CC\u6682\u65E0\u7B14\u8BB0\u3002', matched: "\u5339\u914D\u5230 {{count}} \u6761\u8BB0\u5F55", noMatch: "\u6CA1\u6709\u5339\u914D\u7B5B\u9009\u6761\u4EF6\u7684\u7B14\u8BB0\u3002", preset: "\u9884\u8BBE", presetScopeDesc: "\u9884\u8BBE\u53EA\u4FDD\u5B58\u5F53\u524D\u9875\u9762\u4E0B\u7684\u53EF\u53D8\u89C6\u56FE\u72B6\u6001\uFF1A\u7B5B\u9009\u3001\u6392\u5E8F\u3001\u663E\u793A\u6A21\u5F0F\uFF08\u65F6\u95F4\u8F74/\u770B\u677F\uFF09\u3002", noPreset: "(\u4E0D\u4F7F\u7528\u9884\u8BBE)", savePreset: "\u4FDD\u5B58\u9884\u8BBE", delPreset: "\u5220\u9664\u9884\u8BBE", presetPrompt: "\u9884\u8BBE\u540D\u79F0", presetNameRequired: "\u8BF7\u8F93\u5165\u9884\u8BBE\u540D\u79F0", presetSaveHint: "\u8BF4\u660E\uFF1A\u540D\u79F0\u4E0D\u53D8=\u8986\u76D6\u66F4\u65B0\u5F53\u524D\u9884\u8BBE\uFF1B\u540D\u79F0\u6539\u4E3A\u65B0\u540D\u5B57=\u53E6\u5B58\u4E3A\u65B0\u9884\u8BBE\uFF0C\u65E7\u9884\u8BBE\u4FDD\u6301\u4E0D\u53D8\u3002", clear: "\u6E05\u7A7A", noneSelected: "(\u5168\u4E0D\u9009)", allText: "(\u5168\u90E8)", selectAll: "\u5168\u9009", selectNone: "\u5168\u4E0D\u9009", selected: "\u5DF2\u9009 {{count}} \u9879", allWhenEmpty: "\u672A\u9009\u62E9\uFF0C\u7B49\u540C\u4E8E\u5168\u90E8", search: "\u641C\u7D22\u9009\u9879...", saveOrder: "\u4FDD\u5B58\u5F53\u524D\u6392\u5E8F", saveOrderDone: "\u5DF2\u4FDD\u5B58\u5F53\u524D\u987A\u5E8F", resetOrder: "\u6062\u590D\u9ED8\u8BA4\u987A\u5E8F", resetOrderDone: "\u5DF2\u6062\u590D\u9ED8\u8BA4\u987A\u5E8F", undoLastAction: "\u64A4\u9500\u4E0A\u4E00\u6B65", undoDone: "\u5DF2\u64A4\u9500\u4E0A\u4E00\u6B65\u64CD\u4F5C", undoNone: "\u6CA1\u6709\u53EF\u64A4\u9500\u7684\u64CD\u4F5C", boardUnset: "(\u672A\u8BBE\u7F6E)", emptyValue: "\u7A7A\u503C", moveAsk: '\u8DE8\u5217\u62D6\u62FD\uFF1A\u662F\u5426\u5C06 {{field}} \u66F4\u65B0\u4E3A "{{value}}"\uFF1F\n\u786E\u5B9A=\u81EA\u52A8\u63A8\u8350\uFF0C\u53D6\u6D88=\u624B\u52A8\u8F93\u5165', moveManual: "\u8BF7\u8F93\u5165 {{field}} \u65B0\u503C\uFF08\u7559\u7A7A\u6E05\u9664\uFF09\uFF1A", moveDone: "\u5C5E\u6027\u5DF2\u66F4\u65B0\uFF1A{{field}} = {{value}}", moveFail: "\u5C5E\u6027\u66F4\u65B0\u5931\u8D25", timelineMoveTitle: "\u65F6\u95F4\u8F74\u62D6\u52A8", timelineMoveChoice: "\u8BF7\u9009\u62E9\u62D6\u52A8\u540E\u7684\u5904\u7406\u65B9\u5F0F\uFF08\u5B57\u6BB5\uFF1A{{field}}\uFF09", timelineMoveCancel: "\u53D6\u6D88\uFF08\u4E0D\u4FDD\u5B58\u4FEE\u6539\uFF09", timelineMoveOrderOnly: "\u4EC5\u66F4\u65B0\u65F6\u95F4\u8F74\u987A\u5E8F", timelineMoveOrderAndProp: "\u66F4\u65B0\u65F6\u95F4\u8F74\u548C\u5C5E\u6027", timelineUpdatePrompt: "\u8BF7\u8F93\u5165 {{field}} \u65B0\u503C\uFF08\u7559\u7A7A\u6E05\u9664\uFF09\uFF1A", timelineMoveDone: "\u5DF2\u66F4\u65B0\u65F6\u95F4\u8F74\u987A\u5E8F", timelineMoveNoChange: "\u5DF2\u53D6\u6D88\uFF0C\u672A\u4FDD\u5B58\u4FEE\u6539", boardMoveTitle: "\u770B\u677F\u62D6\u52A8", boardMoveChoice: "\u8BF7\u9009\u62E9\u62D6\u52A8\u540E\u7684\u5904\u7406\u65B9\u5F0F\uFF08\u5B57\u6BB5\uFF1A{{field}}\uFF09", boardMoveCancel: "\u53D6\u6D88\uFF08\u4E0D\u4FDD\u5B58\u4FEE\u6539\uFF09", boardMoveOrderOnly: "\u4EC5\u66F4\u65B0\u770B\u677F\u987A\u5E8F", boardMoveOrderAndProp: "\u66F4\u65B0\u770B\u677F\u987A\u5E8F\u548C\u5C5E\u6027", boardMoveDone: "\u5DF2\u66F4\u65B0\u770B\u677F\u987A\u5E8F", boardMoveNoChange: "\u5DF2\u53D6\u6D88\uFF0C\u672A\u4FDD\u5B58\u4FEE\u6539", savePreviewOnClose: "\u5F53\u524D\u4E3A\u9884\u89C8\u914D\u7F6E\uFF0C\u5173\u95ED\u524D\u662F\u5426\u4FDD\u5B58\u4E3A\u9875\u9762\uFF1F", refreshPreview: "\u5237\u65B0\u9884\u89C8", refreshPreviewDone: "\u5361\u7247\u9884\u89C8\u5DF2\u5237\u65B0", exportPage: "\u5BFC\u51FA\u9875\u9762", exportFolderTitle: "\u9009\u62E9\u5BFC\u51FA\u6587\u4EF6\u5939", exportFolderDesc: "\u8BF7\u9009\u62E9\u5BFC\u51FA\u5230\u54EA\u4E2A\u6587\u4EF6\u5939\uFF08\u53EF\u9009\u6839\u6587\u4EF6\u5939\uFF09", exportFolderConfirm: "\u5BFC\u51FA\u5230\u6B64\u6587\u4EF6\u5939", exportPageDone: "\u9875\u9762\u914D\u7F6E\u5DF2\u5BFC\u51FA\uFF1A{{path}}", exportPageFailed: "\u9875\u9762\u5BFC\u51FA\u5931\u8D25", exportBlockTitle: "Smart Folder \u9875\u9762\u5BFC\u51FA", exportBlockDesc: "\u70B9\u51FB\u4E0B\u65B9\u6309\u94AE\u53EF\u6253\u5F00\u8BE5\u9875\u9762\u914D\u7F6E\u3002", exportBlockOpen: "\u6253\u5F00\u6B64\u9875\u9762", exportBlockImport: "\u5BFC\u5165\u5E76\u4FDD\u5B58\u5230\u9875\u9762\u7BA1\u7406", exportBlockInvalid: "\u5BFC\u51FA\u5757\u89E3\u6790\u5931\u8D25", colorSettings: "\u989C\u8272\u8BBE\u7F6E", saturationBase: "\u9971\u548C\u5EA6\u57FA\u7EBF", saturationRange: "\u9971\u548C\u5EA6\u6CE2\u52A8", lightnessBase: "\u4EAE\u5EA6\u57FA\u7EBF", lightnessRange: "\u4EAE\u5EA6\u6CE2\u52A8", fallbackColor: "\u9ED8\u8BA4\u989C\u8272", resetDefault: "\u6062\u590D\u9ED8\u8BA4", languageMode: "\u754C\u9762\u8BED\u8A00", languageAuto: "\u81EA\u52A8\uFF08\u8DDF\u968F Obsidian\uFF09", languageZh: "\u4E2D\u6587", languageEn: "English", languageModeDesc: "\u9ED8\u8BA4\u81EA\u52A8\u5339\u914D\uFF1B\u4F60\u4E5F\u53EF\u4EE5\u624B\u52A8\u56FA\u5B9A\u8BED\u8A00\u3002" }, en: { title: "Smart Folder View", openBuilder: "Create Smart Folder page", openManager: "Manage saved pages", openLast: "Open last page", noProfile: "No saved pages yet. Please create one first.", setupTitle: "Configure page", setupDesc: "Page settings anchor data source: folder, template file, and filterable fields. Preview first, then save.", profileName: "Page name", sourceFolder: "Source folder", sourceFolderDesc: "Load cards from this folder", templateFile: "Template file", templateFileDesc: "Choose a template file from the whole vault; fields come from it plus common folder fields", colorField: "Color field", sortField: "Sort field", displayFields: "Display fields", sortOrder: "Sort order", viewMode: "View mode", boardField: "Board group field", preview: "Preview", saveAndOpen: "Save and Open", choose: "(Choose)", rootFolder: "(Vault Root)", timeline: "Timeline", board: "Board", asc: "Ascending (old -> new)", desc: "Descending (new -> old)", savePage: "Save page", updatePage: "Update page", openBuilderBtn: "Create page", openManagerBtn: "Page manager", startActionTitle: "Smart Folder", startActionDesc: "Choose an action:", startOpenPage: "Open page", startNewPage: "Create page", pageSaved: "Page saved", pageUpdated: "Page updated", saveFailed: "Failed to save page", pageNamePrompt: "Enter page name", managerTitle: "Saved pages", open: "Open", edit: "Edit", delete: "Delete", deleteConfirm: "Delete page: {{name}}?", noFiles: 'No notes found in "{{folder}}".', matched: "{{count}} notes matched", noMatch: "No notes match current filters.", preset: "Preset", presetScopeDesc: "Presets only save mutable view state in this page: filters, sort, and view mode (timeline/board).", noPreset: "(No preset)", savePreset: "Save preset", delPreset: "Delete preset", presetPrompt: "Preset name", presetNameRequired: "Please enter a preset name", presetSaveHint: "Rule: same name = update this preset; new name = save as a new preset and keep the old one unchanged.", clear: "Clear", noneSelected: "(None selected)", allText: "(All)", selectAll: "Select all", selectNone: "Select none", selected: "{{count}} selected", allWhenEmpty: "Nothing selected, equivalent to all", search: "Search options...", saveOrder: "Save current order", saveOrderDone: "Current order saved", resetOrder: "Reset default order", resetOrderDone: "Default order restored", undoLastAction: "Undo last action", undoDone: "Last action undone", undoNone: "Nothing to undo", boardUnset: "(Unset)", emptyValue: "empty", moveAsk: 'Cross-column move: update {{field}} to "{{value}}"?\nOK=recommended, Cancel=manual input', moveManual: "Enter new value for {{field}} (empty to clear):", moveDone: "Updated: {{field}} = {{value}}", moveFail: "Failed to update property", timelineMoveTitle: "Timeline move", timelineMoveChoice: "Choose how to apply this move (field: {{field}})", timelineMoveCancel: "Cancel (do not save)", timelineMoveOrderOnly: "Update timeline order only", timelineMoveOrderAndProp: "Update timeline order and property", timelineUpdatePrompt: "Enter new value for {{field}} (empty to clear):", timelineMoveDone: "Timeline order updated", timelineMoveNoChange: "Canceled, no changes saved", boardMoveTitle: "Board move", boardMoveChoice: "Choose how to apply this move (field: {{field}})", boardMoveCancel: "Cancel (do not save)", boardMoveOrderOnly: "Update board order only", boardMoveOrderAndProp: "Update board order and property", boardMoveDone: "Board order updated", boardMoveNoChange: "Canceled, no changes saved", savePreviewOnClose: "This is a preview configuration. Save it before closing?", refreshPreview: "Refresh preview", refreshPreviewDone: "Card preview refreshed", exportPage: "Export page", exportFolderTitle: "Choose export folder", exportFolderDesc: "Select where to export (including vault root).", exportFolderConfirm: "Export here", exportPageDone: "Page config exported: {{path}}", exportPageFailed: "Failed to export page", exportBlockTitle: "Smart Folder page export", exportBlockDesc: "Click the button below to open this page configuration.", exportBlockOpen: "Open this page", exportBlockImport: "Import and save to page manager", exportBlockInvalid: "Failed to parse export block", colorSettings: "Color settings", saturationBase: "Saturation base", saturationRange: "Saturation range", lightnessBase: "Lightness base", lightnessRange: "Lightness range", fallbackColor: "Fallback color", resetDefault: "Reset default", languageMode: "UI language", languageAuto: "Auto (follow Obsidian)", languageZh: "Chinese", languageEn: "English", languageModeDesc: "Auto by default; you can also force a fixed language." } }; function tpl(text, vars) { if (!vars) return text; return text.replace(/\{\{(\w+)\}\}/g, (_m, key) => String(vars[key] ?? "")); } function detectLanguage() { try { const locale = window.moment?.locale?.() || ""; if (locale.toLowerCase().startsWith("zh")) return "zh"; } catch { } return "en"; } function newId() { return `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; } function hashCode(str) { let h = 0; for (let i = 0; i < str.length; i++) { h = (h << 5) - h + str.charCodeAt(i); h |= 0; } return Math.abs(h); } function clamp(value, min, max) { return Math.max(min, Math.min(max, value)); } function colorForValue(value, config) { if (!value) return null; const hash = hashCode(value); const h = hash % 360; const s = clamp(config.saturationBase + hash % Math.max(1, config.saturationRange), 20, 90); const l = clamp(config.lightnessBase + hash % Math.max(1, config.lightnessRange), 20, 80); return `hsl(${h} ${s}% ${l}%)`; } function uniqueSorted(items) { return [...new Set(items)].sort((a, b) => a.localeCompare(b, "zh-Hans-CN")); } function toFlatArray(value) { if (value == null) return []; if (Array.isArray(value)) return value.flatMap((v) => toFlatArray(v)); if (typeof value === "string") return [value.trim()].filter(Boolean); if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") { return [`${value}`.trim()].filter(Boolean); } return []; } function getFrontmatterValues(cache, key) { if (!cache?.frontmatter) return []; return toFlatArray(cache.frontmatter[key]); } function getAllFrontmatterKeys(caches) { const keys = /* @__PURE__ */ new Set(); for (const cache of caches) { if (cache?.frontmatter) { for (const key of Object.keys(cache.frontmatter)) { if (key !== "position") keys.add(key); } } } return uniqueSorted([...keys]); } function stripFrontmatter(text) { return text.replace(/^---\n[\s\S]*?\n---\n?/, "").trim(); } function parseDateLike(value) { const raw = String(value || "").trim(); if (!raw) return null; const toChrono = (year, month = 1, day = 1) => { const safeMonth = clamp(Number(month) || 1, 1, 12); const safeDay = clamp(Number(day) || 1, 1, 31); return Number(year) + (safeMonth - 1) / 12 + (safeDay - 1) / 366; }; const compact = raw.replace(/[,\s,]/g, ""); const plainNumber = compact.match(/^-?\d+(?:\.\d+)?$/); if (plainNumber) return Number(plainNumber[0]); const relative = compact.match(/^(?:约|大约|距今)?(?:公元前|前)?(\d+(?:\.\d+)?)(亿|万)?年?(?:前)$/i) || compact.match(/^(?:约|大约)?(?:公元前|前)(\d+(?:\.\d+)?)(亿|万)?年?$/i); if (relative) { const unit = relative[2] === "\u4EBF" ? 1e8 : relative[2] === "\u4E07" ? 1e4 : 1; return -Number(relative[1]) * unit; } const centuryDecade = compact.match(/^(\d{1,2})世纪(\d{1,2})年代$/); if (centuryDecade) return (Number(centuryDecade[1]) - 1) * 100 + Number(centuryDecade[2]); const decade = compact.match(/^(-?\d{1,9})年代$/); if (decade) return Number(decade[1]); const normalized = raw.replace(/[年./]/g, "-").replace(/月/g, "-").replace(/日/g, "").replace(/\s+/g, "").trim(); const ymd = normalized.match(/^(-?\d{1,9})(?:-(\d{1,4})(?:-(\d{1,2}))?)?/); if (ymd) { const year = Number(ymd[1]); const second = ymd[2] ? Number(ymd[2]) : null; if (second == null || second > 12) return year; return toChrono(year, second, ymd[3] || 1); } return null; } function isChronologicalSortField(field) { const normalized = String(field || "").toLowerCase().replace(/\s+/g, ""); return /时间|日期|发生于|年份|年代|date|time|year|created|updated/.test(normalized); } function parseSortValue(value, sortField = "") { const raw = String(value == null ? "" : value).trim(); if (isChronologicalSortField(sortField)) { const ts = parseDateLike(raw); if (ts != null) return { kind: "number", value: ts, raw }; } const numericText = raw.replace(/,/g, ""); if (/^-?\d+(\.\d+)?$/.test(numericText)) return { kind: "number", value: Number(numericText), raw }; return { kind: "string", value: raw, raw }; } function clonePreset(preset) { return { name: String(preset?.name || ""), enabledFilters: Array.isArray(preset?.enabledFilters) ? [...preset.enabledFilters] : [], selectedByAttr: JSON.parse(JSON.stringify(preset?.selectedByAttr || {})), sortOrder: preset?.sortOrder === "asc" ? "asc" : "desc", viewMode: preset?.viewMode === "board" ? "board" : "timeline", boardField: typeof preset?.boardField === "string" ? preset.boardField : "" }; } function clonePresetList(presets) { return (Array.isArray(presets) ? presets : []).map(clonePreset); } function cloneSelectedByAttr(state) { return JSON.parse(JSON.stringify(state || {})); } function isSmartFolderExportCache(cache) { const fm = cache?.frontmatter; return fm?.type === "smart-folder-view-page-export" || fm?.["smart foldertype"] === "smart-folder-view-page-export"; } function isLikelySmartFolderExportPath(path) { const name = String(path || "").split("/").pop()?.toLowerCase() || ""; return name.endsWith(".smart-folder-page.md") || /smart[-_\s]*folder[-_\s]*page/.test(name) || /smart[-_\s]*foder[-_\s]*page/.test(name); } function makeSelectSearchable(selectEl, placeholder) { void selectEl; void placeholder; } var SetupModal = class extends obsidian.Modal { constructor(app, plugin, profile) { super(app); this.plugin = plugin; this.hasPreviewedDraft = false; this.skipCloseSavePrompt = false; const folders = this.plugin.getFolderOptions(); const sourceFolder = profile?.sourceFolder || folders[0] || ""; const template = profile?.templateFile || this.plugin.getTemplateFileOptions()[0] || ""; this.draft = profile ? { ...profile, displayFields: [...profile.displayFields] } : { id: newId(), name: "", sourceFolder, templateFile: template, colorField: "", displayFields: [], sortField: "", sortOrder: "desc", viewMode: "timeline", boardField: "\u72B6\u6001", createdAt: Date.now(), updatedAt: Date.now() }; } render() { const { contentEl } = this; contentEl.empty(); contentEl.createEl("h2", { text: this.plugin.t("setupTitle") }); contentEl.createEl("p", { text: this.plugin.t("setupDesc") }); const folders = this.plugin.getFolderOptions(); if (!folders.includes(this.draft.sourceFolder)) this.draft.sourceFolder = folders[0] || ""; const templates = this.plugin.getTemplateFileOptions(); if (!templates.includes(this.draft.templateFile)) this.draft.templateFile = templates[0] || ""; const fieldOptions = this.plugin.getFieldCandidates(this.draft.sourceFolder, this.draft.templateFile, this.draft.sourceNotePath); if (!fieldOptions.includes(this.draft.colorField)) this.draft.colorField = fieldOptions[0] || ""; if (!fieldOptions.includes(this.draft.sortField)) this.draft.sortField = fieldOptions[0] || ""; if (!fieldOptions.includes(this.draft.boardField)) { this.draft.boardField = fieldOptions.includes("\u72B6\u6001") ? "\u72B6\u6001" : fieldOptions[0] || ""; } this.draft.displayFields = this.draft.displayFields.filter((k) => fieldOptions.includes(k)); if (!this.draft.displayFields.length) this.draft.displayFields = fieldOptions.slice(0, 4); new obsidian.Setting(contentEl).setName(this.plugin.t("profileName")).addText((t) => t.setValue(this.draft.name).onChange((v) => { this.draft.name = v.trim(); })); new obsidian.Setting(contentEl).setName(this.plugin.t("sourceFolder")).setDesc(this.plugin.t("sourceFolderDesc")).addDropdown((d) => { for (const f of folders) d.addOption(f, f || this.plugin.t("rootFolder")); d.setValue(this.draft.sourceFolder || ""); makeSelectSearchable(d.selectEl, this.plugin.t("search")); d.onChange((v) => { this.draft.sourceFolder = v; this.render(); }); }); new obsidian.Setting(contentEl).setName(this.plugin.t("templateFile")).setDesc(this.plugin.t("templateFileDesc")).addDropdown((d) => { d.addOption("", this.plugin.t("choose")); for (const f of templates) d.addOption(f, f); d.setValue(this.draft.templateFile || ""); makeSelectSearchable(d.selectEl, this.plugin.t("search")); d.onChange((v) => { this.draft.templateFile = v; this.render(); }); }); new obsidian.Setting(contentEl).setName(this.plugin.t("colorField")).addDropdown((d) => { d.addOption("", this.plugin.t("choose")); for (const k of fieldOptions) d.addOption(k, k); d.setValue(this.draft.colorField || ""); makeSelectSearchable(d.selectEl, this.plugin.t("search")); d.onChange((v) => { this.draft.colorField = v; }); }); new obsidian.Setting(contentEl).setName(this.plugin.t("sortField")).addDropdown((d) => { d.addOption("", this.plugin.t("choose")); for (const k of fieldOptions) d.addOption(k, k); d.setValue(this.draft.sortField || ""); makeSelectSearchable(d.selectEl, this.plugin.t("search")); d.onChange((v) => { this.draft.sortField = v; }); }); new obsidian.Setting(contentEl).setName(this.plugin.t("displayFields")).addText((t) => t.setValue(this.draft.displayFields.join(", ")).onChange((v) => { this.draft.displayFields = v.split(",").map((s) => s.trim()).filter(Boolean); })); new obsidian.Setting(contentEl).setName(this.plugin.t("sortOrder")).addDropdown((d) => { d.addOption("desc", this.plugin.t("desc")); d.addOption("asc", this.plugin.t("asc")); d.setValue(this.draft.sortOrder); makeSelectSearchable(d.selectEl, this.plugin.t("search")); d.onChange((v) => { this.draft.sortOrder = v; }); }); new obsidian.Setting(contentEl).setName(this.plugin.t("viewMode")).addDropdown((d) => { d.addOption("timeline", this.plugin.t("timeline")); d.addOption("board", this.plugin.t("board")); d.setValue(this.draft.viewMode); makeSelectSearchable(d.selectEl, this.plugin.t("search")); d.onChange((v) => { this.draft.viewMode = v === "board" ? "board" : "timeline"; this.render(); }); }); new obsidian.Setting(contentEl).setName(this.plugin.t("boardField")).addDropdown((d) => { d.addOption("", this.plugin.t("choose")); for (const k of fieldOptions) d.addOption(k, k); d.setValue(this.draft.boardField || ""); makeSelectSearchable(d.selectEl, this.plugin.t("search")); d.onChange((v) => { this.draft.boardField = v; }); }); const action = contentEl.createEl("div"); action.addClass("sfv-setup-action"); action.createEl("button", { text: this.plugin.t("preview") }).addEventListener("click", () => { this.plugin.setDraftProfile(this.draft); void this.plugin.activateView(); this.hasPreviewedDraft = true; }); const saveBtn = action.createEl("button", { text: this.plugin.t("saveAndOpen") }); saveBtn.addClass("mod-cta"); saveBtn.addEventListener("click", () => { void (async () => { await this.plugin.saveProfile(this.draft, true, false); await this.plugin.activateView({ profile: this.draft }); this.skipCloseSavePrompt = true; this.close(); })(); }); } onOpen() { this.render(); } onClose() { this.contentEl.empty(); if (this.skipCloseSavePrompt || !this.hasPreviewedDraft) return; void askConfirm(this.app, this.plugin.t("savePreviewOnClose")).then((ok) => { if (!ok) return; void this.plugin.saveProfile(this.draft, true, false).then( () => void this.plugin.activateView({ profile: this.draft }) ); }); } }; var TextInputModal = class extends obsidian.Modal { constructor(app, title, placeholder, initial, onSubmit, hint) { super(app); this.titleText = title; this.hintText = hint; this.placeholderText = placeholder; this.initialValue = initial; this.onSubmitValue = onSubmit; this.resolved = false; } submit(value) { if (this.resolved) return; this.resolved = true; this.onSubmitValue(value); } onOpen() { const { contentEl } = this; contentEl.empty(); contentEl.createEl("h3", { text: this.titleText }); if (this.hintText) contentEl.createEl("p", { text: this.hintText }).addClass("sfv-modal-hint"); const input = contentEl.createEl("input"); input.type = "text"; input.placeholder = this.placeholderText; input.value = this.initialValue; input.addClass("sfv-modal-input"); const action = contentEl.createEl("div"); action.addClass("sfv-modal-action"); action.createEl("button", { text: "Cancel" }).addEventListener("click", () => { this.submit(null); this.close(); }); const okBtn = action.createEl("button", { text: "OK" }); okBtn.addClass("mod-cta"); okBtn.addEventListener("click", () => { this.submit(input.value.trim()); this.close(); }); input.addEventListener("keydown", (e) => { if (e.key === "Enter") { e.preventDefault(); this.submit(input.value.trim()); this.close(); } }); window.setTimeout(() => input.focus(), 0); } onClose() { this.submit(null); this.contentEl.empty(); } }; function askTextInput(app, title, placeholder, initial, hint) { return new Promise((resolve) => { new TextInputModal(app, title, placeholder, initial, resolve, hint).open(); }); } var ThreeChoiceModal = class extends obsidian.Modal { constructor(app, title, message, labels, onSubmit) { super(app); this.titleText = title; this.messageText = message; this.labels = labels; this.onSubmitValue = onSubmit; } onOpen() { const { contentEl } = this; contentEl.empty(); contentEl.createEl("h3", { text: this.titleText }); contentEl.createEl("p", { text: this.messageText }).addClass("sfv-modal-hint"); const action = contentEl.createEl("div"); action.addClass("sfv-modal-action"); const addChoice = (key, label, isCta = false) => { if (!label) return; const btn = action.createEl("button", { text: label }); if (isCta) btn.addClass("mod-cta"); btn.addEventListener("click", () => { this.onSubmitValue(key); this.close(); }); }; addChoice("cancel", this.labels.cancel); addChoice("order", this.labels.order); addChoice("order+prop", this.labels.orderAndProp, true); } onClose() { this.contentEl.empty(); } }; function askThreeChoices(app, title, message, labels) { return new Promise((resolve) => { new ThreeChoiceModal(app, title, message, labels, resolve).open(); }); } var StartActionModal = class extends obsidian.Modal { constructor(app, plugin, onSubmit) { super(app); this.plugin = plugin; this.onSubmitValue = onSubmit; } onOpen() { const { contentEl } = this; contentEl.empty(); contentEl.createEl("h3", { text: this.plugin.t("startActionTitle") }); contentEl.createEl("p", { text: this.plugin.t("startActionDesc") }).addClass("sfv-modal-hint"); const action = contentEl.createEl("div"); action.addClass("sfv-modal-action"); action.createEl("button", { text: this.plugin.t("startOpenPage") }).addEventListener("click", () => { this.onSubmitValue("open"); this.close(); }); const createBtn = action.createEl("button", { text: this.plugin.t("startNewPage") }); createBtn.addClass("mod-cta"); createBtn.addEventListener("click", () => { this.onSubmitValue("create"); this.close(); }); } onClose() { this.contentEl.empty(); } }; function askStartAction(app, plugin) { return new Promise((resolve) => { new StartActionModal(app, plugin, resolve).open(); }); } var FolderSelectModal = class extends obsidian.Modal { constructor(app, plugin, folders, initialValue, onSubmit) { super(app); this.plugin = plugin; this.folders = folders; this.initialValue = initialValue; this.onSubmitValue = onSubmit; } onOpen() { const { contentEl } = this; contentEl.empty(); contentEl.createEl("h3", { text: this.plugin.t("exportFolderTitle") }); contentEl.createEl("p", { text: this.plugin.t("exportFolderDesc") }).addClass("sfv-modal-hint"); const select = contentEl.createEl("select"); select.addClass("sfv-modal-select"); for (const f of this.folders) { select.createEl("option", { value: f, text: f || this.plugin.t("rootFolder") }); } select.value = this.initialValue; makeSelectSearchable(select, this.plugin.t("search")); const action = contentEl.createEl("div"); action.addClass("sfv-modal-action"); action.createEl("button", { text: "Cancel" }).addEventListener("click", () => { this.onSubmitValue(null); this.close(); }); const okBtn = action.createEl("button", { text: this.plugin.t("exportFolderConfirm") }); okBtn.addClass("mod-cta"); okBtn.addEventListener("click", () => { this.onSubmitValue(select.value); this.close(); }); } onClose() { this.contentEl.empty(); } }; function askFolderSelect(app, plugin, folders, initialValue) { return new Promise((resolve) => { new FolderSelectModal(app, plugin, folders, initialValue, resolve).open(); }); } var ConfirmModal = class extends obsidian.Modal { constructor(app, message, onResult) { super(app); this.message = message; this.onResultValue = onResult; } onOpen() { const { contentEl } = this; contentEl.empty(); contentEl.createEl("p", { text: this.message }); const action = contentEl.createEl("div"); action.addClass("sfv-modal-action"); action.createEl("button", { text: "Cancel" }).addEventListener("click", () => { this.onResultValue(false); this.close(); }); const ok = action.createEl("button", { text: "OK" }); ok.addClass("mod-cta"); ok.addEventListener("click", () => { this.onResultValue(true); this.close(); }); } onClose() { this.contentEl.empty(); } }; function askConfirm(app, message) { return new Promise((resolve) => { new ConfirmModal(app, message, resolve).open(); }); } var ProfileManagerModal = class extends obsidian.Modal { constructor(app, plugin) { super(app); this.plugin = plugin; } onOpen() { const { contentEl } = this; contentEl.empty(); contentEl.createEl("h2", { text: this.plugin.t("managerTitle") }); for (const profile of this.plugin.data.profiles) { const s = new obsidian.Setting(contentEl).setName(profile.name || profile.sourceFolder).setDesc(profile.sourceFolder); s.addButton((b) => b.setButtonText(this.plugin.t("open")).onClick(() => { void (async () => { this.plugin.clearDraftProfile(); this.plugin.data.lastActiveProfileId = profile.id; await this.plugin.persist(); await this.plugin.activateView({ profile, newLeaf: true }); this.close(); })(); })); s.addButton((b) => b.setButtonText(this.plugin.t("edit")).onClick(() => { new SetupModal(this.app, this.plugin, profile).open(); })); s.addButton((b) => b.setButtonText(this.plugin.t("delete")).onClick(() => { void askConfirm(this.app, this.plugin.t("deleteConfirm", { name: profile.name || profile.sourceFolder })).then(async (confirmed) => { if (!confirmed) return; this.plugin.data.profiles = this.plugin.data.profiles.filter((p) => p.id !== profile.id); if (this.plugin.data.lastActiveProfileId === profile.id) this.plugin.data.lastActiveProfileId = ""; await this.plugin.persist(); this.onOpen(); }); })); } } onClose() { this.contentEl.empty(); } }; var SmartFolderView = class extends obsidian.ItemView { constructor(leaf, plugin) { super(leaf); this.plugin = plugin; this.pinnedProfile = null; this.stateOwnerId = ""; this.profileDefaultsKey = ""; this.timelineDropBusy = false; this.timelineDragToken = ""; this.pendingBoardFieldValueByPath = {}; this.lastUndoSnapshot = null; } getViewType() { return VIEW_TYPE; } getDisplayText() { const p = this.pinnedProfile || this.plugin.getCurrentProfile(); return p?.name || p?.sourceFolder || this.plugin.t("title"); } getIcon() { return "layout-grid"; } updateLeafTitle() { const title = this.getDisplayText(); const leafAny = this.leaf; if (leafAny?.setTitle) leafAny.setTitle(title); if (leafAny?.tabHeaderInnerTitleEl?.setText) leafAny.tabHeaderInnerTitleEl.setText(title); } setPinnedProfile(profile) { if (!profile) { this.pinnedProfile = null; } else { this.pinnedProfile = { ...profile, displayFields: [...profile.displayFields || []] }; } this.stateOwnerId = ""; this.profileDefaultsKey = ""; this.lastUndoSnapshot = null; this.updateLeafTitle(); } getRenderProfile() { if (!this.pinnedProfile) return this.plugin.getCurrentProfile(); const latest = this.plugin.data.profiles.find((p) => p.id === this.pinnedProfile.id) || this.pinnedProfile; this.pinnedProfile = { ...latest, sourceNotePath: this.pinnedProfile.sourceNotePath || latest.sourceNotePath || "", displayFields: [...latest.displayFields || []] }; return this.pinnedProfile; } ensureState(ownerId, defaultSort, defaultViewMode) { const nextDefaultsKey = `${defaultSort}|${defaultViewMode}`; if (this.stateOwnerId === ownerId && this.profileDefaultsKey === nextDefaultsKey) return; this.stateOwnerId = ownerId; this.profileDefaultsKey = nextDefaultsKey; try { const saved = this.app.loadLocalStorage(FILTER_STORAGE_PREFIX + ownerId) || {}; this.isFreshFilterState = !saved || Object.keys(saved).length === 0; const savedDefaultsKey = typeof saved.profileDefaultsKey === "string" ? saved.profileDefaultsKey : ""; const defaultsChanged = !!savedDefaultsKey && savedDefaultsKey !== this.profileDefaultsKey; this.filterState = { enabledFilters: Array.isArray(saved.enabledFilters) ? saved.enabledFilters : [], selectedByAttr: saved.selectedByAttr && typeof saved.selectedByAttr === "object" ? saved.selectedByAttr : {}, sortOrder: defaultsChanged ? defaultSort : saved.sortOrder === "asc" || saved.sortOrder === "desc" ? saved.sortOrder : defaultSort, viewMode: defaultsChanged ? defaultViewMode : saved.viewMode === "board" || saved.viewMode === "timeline" ? saved.viewMode : defaultViewMode, presetName: typeof saved.presetName === "string" ? saved.presetName : "", boardField: typeof saved.boardField === "string" ? saved.boardField : null }; } catch { this.isFreshFilterState = true; this.filterState = { enabledFilters: [], selectedByAttr: {}, sortOrder: defaultSort, viewMode: defaultViewMode, presetName: "", boardField: null }; } } saveState() { this.app.saveLocalStorage(FILTER_STORAGE_PREFIX + this.stateOwnerId, { ...this.filterState, profileDefaultsKey: this.profileDefaultsKey }); } loadPresets() { try { const parsed = this.app.loadLocalStorage(FILTER_PRESET_STORAGE_PREFIX + this.stateOwnerId); if (!Array.isArray(parsed)) return []; return clonePresetList(parsed); } catch { return []; } } savePresets(presets) { this.app.saveLocalStorage(FILTER_PRESET_STORAGE_PREFIX + this.stateOwnerId, clonePresetList(presets)); } contextKey(profile) { return JSON.stringify({ profileId: profile.id, selectedByAttr: this.filterState.selectedByAttr, enabled: this.filterState.enabledFilters, sort: this.filterState.sortOrder, viewMode: this.filterState.viewMode, boardField: profile.boardField }); } orderKey(profile, column) { return `${this.contextKey(profile)}:${column || "timeline"}`; } loadOrderStore(ownerId) { try { const parsed = this.app.loadLocalStorage(MANUAL_ORDER_STORAGE_PREFIX + ownerId); return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {}; } catch { return {}; } } saveOrderStore(ownerId, store) { this.app.saveLocalStorage(MANUAL_ORDER_STORAGE_PREFIX + ownerId, store); } applyOrder(ownerId, key, items) { const store = this.loadOrderStore(ownerId); const order = store[key] || []; if (!order.length) return items; const rank = /* @__PURE__ */ new Map(); order.forEach((p, i) => rank.set(p, i)); return [...items].sort((a, b) => (rank.get(a.file.path) ?? Number.MAX_SAFE_INTEGER) - (rank.get(b.file.path) ?? Number.MAX_SAFE_INTEGER)); } saveOrder(ownerId, key, files) { const store = this.loadOrderStore(ownerId); store[key] = [...new Set(files.map((f) => f.path))]; this.saveOrderStore(ownerId, store); } clearOrder(ownerId, key) { const store = this.loadOrderStore(ownerId); delete store[key]; this.saveOrderStore(ownerId, store); } recordUndoSnapshot(profile) { this.lastUndoSnapshot = { ownerId: profile.id, orderStore: this.loadOrderStore(profile.id), pendingBoardFieldValueByPath: { ...this.pendingBoardFieldValueByPath } }; } async undoLastAction(profile) { if (!this.lastUndoSnapshot || this.lastUndoSnapshot.ownerId !== profile.id) { new obsidian.Notice(this.plugin.t("undoNone")); return; } this.saveOrderStore(profile.id, this.lastUndoSnapshot.orderStore || {}); this.pendingBoardFieldValueByPath = { ...this.lastUndoSnapshot.pendingBoardFieldValueByPath || {} }; this.lastUndoSnapshot = null; await this.rerenderPreserveScroll(); new obsidian.Notice(this.plugin.t("undoDone")); } onOpen() { this.renderView(); } async rerenderPreserveScroll() { const container = this.containerEl.children[1]; const scrollTop = container?.scrollTop || 0; this.renderView(); const nextContainer = this.containerEl.children[1]; await new Promise((resolve) => window.requestAnimationFrame(() => window.requestAnimationFrame(resolve))); if (nextContainer) nextContainer.scrollTop = scrollTop; } async askAndMaybeUpdateField(file, field, targetColumn, unsetLabel) { const suggested = targetColumn === unsetLabel ? "" : targetColumn; const useSuggested = await askConfirm(this.app, this.plugin.t("moveAsk", { field, value: suggested || this.plugin.t("emptyValue") })); const finalValue = useSuggested ? suggested : await askTextInput(this.app, this.plugin.t("moveManual", { field }), this.plugin.t("moveManual", { field }), suggested, ""); if (finalValue == null) return void 0; const value = finalValue.trim(); try { await this.app.fileManager.processFrontMatter(file, (fm) => { if (value) fm[field] = value; else delete fm[field]; }); new obsidian.Notice(this.plugin.t("moveDone", { field, value: value || this.plugin.t("emptyValue") })); return value; } catch { new obsidian.Notice(this.plugin.t("moveFail")); return void 0; } } getBoardFieldValue(file, cache, boardField) { if (Object.prototype.hasOwnProperty.call(this.pendingBoardFieldValueByPath, file.path)) { return this.pendingBoardFieldValueByPath[file.path] || ""; } return getFrontmatterValues(cache, boardField)[0] || ""; } getBoardFieldValues(file, cache, boardField) { if (Object.prototype.hasOwnProperty.call(this.pendingBoardFieldValueByPath, file.path)) { return toFlatArray(this.pendingBoardFieldValueByPath[file.path]); } return getFrontmatterValues(cache, boardField); } reconcilePendingBoardValue(file, cache, boardField) { if (!Object.prototype.hasOwnProperty.call(this.pendingBoardFieldValueByPath, file.path)) return; const pending = this.pendingBoardFieldValueByPath[file.path] || ""; const actual = getFrontmatterValues(cache, boardField)[0] || ""; if (pending === actual) delete this.pendingBoardFieldValueByPath[file.path]; } renderCard(parent, file, cache, profile, color, showDot) { const card = parent.createEl("section"); card.addClass("sfv-card"); card.setCssProps({ "--sfv-color": color }); if (showDot) { card.createEl("span").addClass("sfv-card-dot"); } const top = card.createEl("div"); top.addClass("sfv-card-top"); const meta = top.createEl("span"); meta.setText(profile.displayFields.map((k) => `${k}: ${getFrontmatterValues(cache, k).join("\u3001") || "-"}`).join(" | ")); meta.addClass("sfv-card-meta"); const link = top.createEl("a"); link.setText(file.basename); link.className = "internal-link"; link.setAttribute("data-href", file.path); link.setAttribute("href", file.path); link.addClass("sfv-card-link"); const triggerHoverPreview = (e) => { this.app.workspace?.trigger?.("hover-link", { event: e, source: VIEW_TYPE, hoverParent: card, targetEl: link, linktext: file.path }); }; link.addEventListener("mouseenter", triggerHoverPreview); link.addEventListener("mousemove", triggerHoverPreview); link.addEventListener("mouseover", triggerHoverPreview); link.addEventListener("pointerenter", triggerHoverPreview); link.addEventListener("pointermove", triggerHoverPreview); link.addEventListener("click", (e) => { e.preventDefault(); void this.app.workspace.openLinkText(file.path, file.path, e.ctrlKey || e.metaKey); }); const triggerHoverPreviewForTarget = (e, targetEl) => { const linktext = targetEl.getAttribute("data-href") || targetEl.getAttribute("href") || targetEl.textContent?.trim() || ""; if (!linktext) return; this.app.workspace?.trigger?.("hover-link", { event: e, source: VIEW_TYPE, hoverParent: card, targetEl, linktext }); }; const getInternalLinkElement = (target) => { if (target instanceof Element) return target.closest(".internal-link"); if (target instanceof Node && target.parentElement) return target.parentElement.closest(".internal-link"); return null; }; const bodyEl = card.createEl("div"); bodyEl.addClass("sfv-card-body"); bodyEl.addEventListener("mouseover", (e) => { const el = getInternalLinkElement(e.target); if (!el) return; triggerHoverPreviewForTarget(e, el); }, true); bodyEl.addEventListener("pointerover", (e) => { const el = getInternalLinkElement(e.target); if (!el) return; triggerHoverPreviewForTarget(e, el); }, true); bodyEl.addEventListener("mousemove", (e) => { const el = getInternalLinkElement(e.target); if (!el) return; triggerHoverPreviewForTarget(e, el); }, true); bodyEl.addEventListener("pointermove", (e) => { const el = getInternalLinkElement(e.target); if (!el) return; triggerHoverPreviewForTarget(e, el); }, true); bodyEl.addEventListener("click", (e) => { const el = getInternalLinkElement(e.target); if (!el) return; const href = el.getAttribute("data-href") || el.getAttribute("href"); if (!href) return; e.preventDefault(); void this.app.workspace.openLinkText(href, file.path, e.ctrlKey || e.metaKey); }, true); void this.app.vault.read(file).then((raw) => { const body = stripFrontmatter(raw) || "\uFF08\u65E0\u6B63\u6587\uFF09"; void obsidian.MarkdownRenderer.render(this.app, body, bodyEl, file.path, this); }).catch(() => bodyEl.setText("\uFF08\u6B63\u6587\u8BFB\u53D6\u5931\u8D25\uFF09")); } buildDropdown(container, label, options, selectedValues, onChange, onCommit) { const wrap = container.createEl("div"); wrap.addClass("sfv-dd-wrap"); wrap.createEl("span", { text: label }).addClass("sfv-dd-label"); const box = wrap.createEl("details"); box.addClass("sfv-dd-box"); const summary = box.createEl("summary"); summary.addClass("sfv-dd-summary"); const initialValues = [...selectedValues]; let currentValues = [...initialValues]; const summarySpan = summary.createEl("span"); summarySpan.addClass("sfv-dd-summary-text"); const toDisplayText = (v) => v === EMPTY_OPTION_VALUE ? this.plugin.t("emptyValue") : v; const refreshSummary = () => { const isAll = currentValues.length === options.length && options.length > 0; const full = !currentValues.length ? this.plugin.t("noneSelected") : currentValues.map(toDisplayText).join("\u3001"); const short = !currentValues.length ? this.plugin.t("noneSelected") : isAll ? this.plugin.t("selectAll") : currentValues.length > 3 ? `${this.plugin.t("selected", { count: currentValues.length })}` : full; summarySpan.setText(short); summarySpan.title = isAll ? this.plugin.t("selectAll") : full; }; refreshSummary(); const panel = box.createEl("div"); panel.addClass("sfv-dd-panel"); const actionBar = panel.createEl("div"); actionBar.addClass("sfv-dd-action-bar"); const allBtn = actionBar.createEl("button", { text: this.plugin.t("selectAll") }); allBtn.addClass("sfv-dd-action-btn"); allBtn.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); currentValues = [...options]; for (const { row } of rows) { const cb = row.querySelector('input[type="checkbox"]'); if (cb) cb.checked = true; } onChange(currentValues); refreshSummary(); }); const clearBtn = actionBar.createEl("button", { text: this.plugin.t("selectNone") }); clearBtn.addClass("sfv-dd-action-btn"); clearBtn.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); currentValues = []; for (const { row } of rows) { const cb = row.querySelector('input[type="checkbox"]'); if (cb) cb.checked = false; } onChange([]); refreshSummary(); }); const search = panel.createEl("input"); search.type = "search"; search.placeholder = this.plugin.t("search"); search.addClass("sfv-dd-search"); const list = panel.createEl("div"); const rows = []; for (const option of options) { const row = list.createEl("label"); row.addClass("sfv-dd-row"); const cb = row.createEl("input"); cb.type = "checkbox"; cb.checked = currentValues.includes(option); row.createEl("span", { text: toDisplayText(option) }); cb.addEventListener("change", () => { const next = cb.checked ? [...currentValues, option] : currentValues.filter((v) => v !== option); currentValues = [...new Set(next)]; onChange(currentValues); refreshSummary(); }); rows.push({ option, row }); } search.addEventListener("input", () => { const q = search.value.toLowerCase(); for (const { option, row } of rows) { if (!q || option.toLowerCase().includes(q)) row.removeClass("sfv-hidden"); else row.addClass("sfv-hidden"); } }); box.addEventListener("toggle", () => { if (box.open) { wrap.addClass("sfv-elevated"); box.addClass("sfv-elevated"); } else { wrap.removeClass("sfv-elevated"); box.removeClass("sfv-elevated"); if (onCommit) onCommit(); } }); } renderView() { const profile = this.getRenderProfile(); const container = this.containerEl.children[1]; container.empty(); container.classList.add("smart-folder-view-root"); this.updateLeafTitle(); if (!profile) { container.createEl("p", { text: this.plugin.t("noProfile") }); return; } this.ensureState(profile.id, profile.sortOrder, profile.viewMode); const topBar = container.createEl("div"); topBar.addClass("sfv-top-bar"); const saveBtn = topBar.createEl("button", { text: this.plugin.isDraft(profile.id) ? this.plugin.t("savePage") : this.plugin.t("updatePage") }); saveBtn.addClass("mod-cta"); saveBtn.addEventListener("click", () => { void (async () => { try { await this.plugin.saveProfile(profile, true, false); await this.rerenderPreserveScroll(); } catch { new obsidian.Notice(this.plugin.t("saveFailed")); } })(); }); topBar.createEl("button", { text: this.plugin.t("openBuilderBtn") }).addEventListener("click", () => new SetupModal(this.app, this.plugin).open()); topBar.createEl("button", { text: this.plugin.t("openManagerBtn") }).addEventListener("click", () => new ProfileManagerModal(this.app, this.plugin).open()); topBar.createEl("button", { text: this.plugin.t("exportPage") }).addEventListener("click", () => { void this.plugin.exportProfileToMarkdown(profile); }); topBar.createEl("button", { text: this.plugin.t("refreshPreview") }).addEventListener("click", () => { this.renderView(); new obsidian.Notice(this.plugin.t("refreshPreviewDone")); }); const files = this.plugin.getFilesByFolder(profile.sourceFolder, profile.sourceNotePath); if (!files.length) { container.createEl("p", { text: this.plugin.t("noFiles", { folder: profile.sourceFolder }) }); return; } if (this.filterState.boardField !== null && this.filterState.boardField !== void 0) { profile.boardField = this.filterState.boardField; } const entries = files.map((file) => ({ file, cache: this.app.metadataCache.getFileCache(file) })); for (const entry of entries) { this.reconcilePendingBoardValue(entry.file, entry.cache, profile.boardField); } const attrKeys = getAllFrontmatterKeys(entries.map((e) => e.cache)); const optionsByAttr = {}; for (const key of attrKeys) { const values = uniqueSorted(entries.flatMap((e) => getFrontmatterValues(e.cache, key))); const hasEmpty = entries.some((e) => getFrontmatterValues(e.cache, key).length === 0); optionsByAttr[key] = hasEmpty ? [...values, EMPTY_OPTION_VALUE] : values; } if (!this.filterState.enabledFilters.length) this.filterState.enabledFilters = attrKeys.slice(0, 4); let defaultSelectionInitialized = false; for (const key of attrKeys) { if (!Array.isArray(this.filterState.selectedByAttr[key])) { this.filterState.selectedByAttr[key] = [...optionsByAttr[key] || []]; defaultSelectionInitialized = true; } } if (defaultSelectionInitialized) this.saveState(); const switchesEl = container.createEl("div"); switchesEl.addClass("sfv-switches"); for (const key of attrKeys) { const enabled = this.filterState.enabledFilters.includes(key); const chip = switchesEl.createEl("label"); chip.addClass("sfv-chip"); const cb = chip.createEl("input"); cb.type = "checkbox"; cb.checked = enabled; chip.createEl("span", { text: key }); cb.addEventListener("change", () => { const s = new Set(this.filterState.enabledFilters); if (cb.checked) s.add(key); else s.delete(key); this.filterState.enabledFilters = attrKeys.filter((k) => s.has(k)); this.saveState(); void this.renderView(); }); } const controls = container.createEl("div"); controls.addClass("sfv-controls"); controls.createEl("p", { text: this.plugin.t("presetScopeDesc") }).addClass("sfv-controls-hint"); const presets = this.loadPresets(); const presetWrap = controls.createEl("div"); presetWrap.addClass("sfv-preset-wrap"); presetWrap.createEl("span", { text: this.plugin.t("preset") }).addClass("sfv-dd-label"); const presetSel = presetWrap.createEl("select"); presetSel.createEl("option", { text: this.plugin.t("noPreset"), value: "" }); for (const p of presets) { const opt = presetSel.createEl("option", { text: p.name, value: p.name }); if (p.name === this.filterState.presetName) opt.selected = true; } presetSel.addEventListener("change", () => { const currentPresets = this.loadPresets(); const p = currentPresets.find((x) => x.name === presetSel.value); if (!p) { this.filterState.presetName = ""; } else { this.filterState.enabledFilters = [...p.enabledFilters]; this.filterState.selectedByAttr = cloneSelectedByAttr(p.selectedByAttr); this.filterState.sortOrder = p.sortOrder; this.filterState.viewMode = p.viewMode; this.filterState.presetName = p.name; this.filterState.boardField = p.boardField || null; if (p.boardField !== void 0) { profile.boardField = p.boardField; const idx = this.plugin.data.profiles.findIndex((q) => q.id === profile.id); if (idx >= 0) this.plugin.data.profiles[idx].boardField = p.boardField; if (this.plugin.draftProfile?.id === profile.id) this.plugin.draftProfile.boardField = p.boardField; void this.plugin.persist(); } } this.saveState(); void this.renderView(); }); makeSelectSearchable(presetSel, this.plugin.t("search")); presetWrap.createEl("button", { text: this.plugin.t("savePreset") }).addEventListener("click", () => { void (async () => { try { const result = await askTextInput(this.app, this.plugin.t("savePreset"), this.plugin.t("presetPrompt"), this.filterState.presetName || "", this.plugin.t("presetSaveHint")); if (result === null) return; const name = result.trim(); if (!name) { new obsidian.Notice(this.plugin.t("presetNameRequired")); return; } const currentPresets = this.loadPresets(); const next = currentPresets.filter((p) => p.name !== name); const currentBoardField = this.filterState.boardField !== null ? this.filterState.boardField : profile.boardField || ""; next.push(clonePreset({ name, enabledFilters: [...this.filterState.enabledFilters], selectedByAttr: cloneSelectedByAttr(this.filterState.selectedByAttr), sortOrder: this.filterState.sortOrder, viewMode: this.filterState.viewMode, boardField: currentBoardField })); this.savePresets(next); this.filterState.presetName = name; this.saveState(); this.renderView(); } catch (e) { console.error("[SmartFolderView] Preset save error:", e); new obsidian.Notice(this.plugin.t("saveFailed")); } })(); }); presetWrap.createEl("button", { text: this.plugin.t("delPreset") }).addEventListener("click", () => { if (!this.filterState.presetName) return; const currentPresets = this.loadPresets(); this.savePresets(currentPresets.filter((p) => p.name !== this.filterState.presetName)); this.filterState.presetName = ""; this.saveState(); void this.renderView(); }); const filterGrid = controls.createEl("div"); filterGrid.addClass("sfv-filter-grid"); for (const key of this.filterState.enabledFilters) { this.buildDropdown(filterGrid, key, optionsByAttr[key] || [], this.filterState.selectedByAttr[key] || [], (vals) => { this.filterState.selectedByAttr[key] = [...vals]; this.saveState(); }, () => { this.renderView(); }); } this.isFreshFilterState = false; const sortWrap = controls.createEl("label"); sortWrap.addClass("sfv-sort-wrap"); sortWrap.createEl("span", { text: this.plugin.t("sortOrder") }).addClass("sfv-dd-label"); const sortSel = sortWrap.createEl("select"); sortSel.createEl("option", { value: "desc", text: this.plugin.t("desc") }); sortSel.createEl("option", { value: "asc", text: this.plugin.t("asc") }); sortSel.value = this.filterState.sortOrder; makeSelectSearchable(sortSel, this.plugin.t("search")); sortSel.dispatchEvent(new Event("change")); sortSel.addEventListener("change", () => { this.filterState.sortOrder = sortSel.value; this.saveState(); void this.renderView(); }); const modeWrap = controls.createEl("label"); modeWrap.addClass("sfv-sort-wrap"); modeWrap.createEl("span", { text: this.plugin.t("viewMode") }).addClass("sfv-dd-label"); const modeSel = modeWrap.createEl("select"); modeSel.createEl("option", { value: "timeline", text: this.plugin.t("timeline") }); modeSel.createEl("option", { value: "board", text: this.plugin.t("board") }); modeSel.value = this.filterState.viewMode; makeSelectSearchable(modeSel, this.plugin.t("search")); modeSel.dispatchEvent(new Event("change")); modeSel.addEventListener("change", () => { this.filterState.viewMode = modeSel.value === "board" ? "board" : "timeline"; this.saveState(); void this.renderView(); }); if (this.filterState.viewMode === "board") { const boardFieldWrap = controls.createEl("label"); boardFieldWrap.addClass("sfv-sort-wrap"); boardFieldWrap.createEl("span", { text: this.plugin.t("boardField") }).addClass("sfv-dd-label"); const boardFieldSel = boardFieldWrap.createEl("select"); boardFieldSel.createEl("option", { value: "", text: this.plugin.t("choose") }); for (const k of attrKeys) boardFieldSel.createEl("option", { value: k, text: k }); const effectiveBoardField = this.filterState.boardField !== null ? this.filterState.boardField : profile.boardField || ""; boardFieldSel.value = effectiveBoardField; makeSelectSearchable(boardFieldSel, this.plugin.t("search")); boardFieldSel.addEventListener("change", () => { const next = boardFieldSel.value; this.filterState.boardField = next; profile.boardField = next; const idx = this.plugin.data.profiles.findIndex((p) => p.id === profile.id); if (idx >= 0) this.plugin.data.profiles[idx].boardField = next; if (this.plugin.draftProfile?.id === profile.id) this.plugin.draftProfile.boardField = next; this.saveState(); void this.plugin.persist(); this.renderView(); }); } const filtered = entries.filter(({ file, cache }) => { if (!this.filterState.enabledFilters.length) return true; for (const key of this.filterState.enabledFilters) { const selected = Array.isArray(this.filterState.selectedByAttr[key]) ? this.filterState.selectedByAttr[key] : [...optionsByAttr[key] || []]; if (!selected.length) return false; const vals = key === profile.boardField ? this.getBoardFieldValues(file, cache, profile.boardField) : getFrontmatterValues(cache, key); const matchEmpty = selected.includes(EMPTY_OPTION_VALUE) && vals.length === 0; const matchNormal = selected.some((v) => v !== EMPTY_OPTION_VALUE && vals.includes(v)); if (!(matchEmpty || matchNormal)) return false; } return true; }); filtered.sort((a, b) => { const va = getFrontmatterValues(a.cache, profile.sortField)[0] || a.file.basename; const vb = getFrontmatterValues(b.cache, profile.sortField)[0] || b.file.basename; const pa = parseSortValue(va, profile.sortField); const pb = parseSortValue(vb, profile.sortField); const cmp = pa.kind === "number" && pb.kind === "number" ? Number(pa.value) - Number(pb.value) : String(pa.raw).localeCompare(String(pb.raw), "zh-Hans-CN", { numeric: true, sensitivity: "base" }); return this.filterState.sortOrder === "asc" ? cmp : -cmp; }); container.createEl("p", { text: this.plugin.t("matched", { count: filtered.length }) }).addClass("sfv-matched"); if (!filtered.length) { container.createEl("p", { text: this.plugin.t("noMatch") }); return; } if (this.filterState.viewMode === "timeline") { const ordered = this.applyOrder(profile.id, this.orderKey(profile), filtered); const orderActions2 = container.createEl("div"); orderActions2.addClass("sfv-order-actions"); orderActions2.createEl("button", { text: this.plugin.t("saveOrder") }).addEventListener("click", () => { this.saveOrder(profile.id, this.orderKey(profile), ordered.map((e) => e.file)); new obsidian.Notice(this.plugin.t("saveOrderDone")); }); orderActions2.createEl("button", { text: this.plugin.t("resetOrder") }).addEventListener("click", () => { void (async () => { this.recordUndoSnapshot(profile); this.clearOrder(profile.id, this.orderKey(profile)); new obsidian.Notice(this.plugin.t("resetOrderDone")); await this.rerenderPreserveScroll(); })(); }); orderActions2.createEl("button", { text: this.plugin.t("undoLastAction") }).addEventListener("click", () => { void this.undoLastAction(profile); }); const timelineEl = container.createEl("div"); timelineEl.addClass("sfv-timeline"); const lineEl = timelineEl.createEl("div"); lineEl.addClass("sfv-timeline-line"); const indicator = timelineEl.createEl("div"); indicator.addClass("sfv-timeline-indicator"); let dropBeforePath = null; const hideIndicator = () => { indicator.removeClass("sfv-visible"); }; const updateDropIndicator = (clientY) => { const cards = Array.from(timelineEl.querySelectorAll("section[data-sfv-path]")).filter((el) => !el.classList.contains("sfv-dragging")); if (!cards.length) { dropBeforePath = null; indicator.setCssProps({ "--sfv-top": "0px" }); indicator.addClass("sfv-visible"); return; } let beforeCard = null; for (const cardEl of cards) { const rect = cardEl.getBoundingClientRect(); if (clientY < rect.top + rect.height / 2) { beforeCard = cardEl; break; } } dropBeforePath = beforeCard?.getAttribute("data-sfv-path") || null; let top = 0; if (beforeCard) { top = beforeCard.offsetTop - 2; } else { const last = cards[cards.length - 1]; top = last.offsetTop + last.offsetHeight - 2; } indicator.setCssProps({ "--sfv-top": `${Math.max(0, top)}px` }); indicator.addClass("sfv-visible"); }; timelineEl.addEventListener("dragover", (e) => { e.preventDefault(); if (e.dataTransfer) e.dataTransfer.dropEffect = "move"; updateDropIndicator(e.clientY); }); timelineEl.addEventListener("dragleave", (e) => { const related = e.relatedTarget; if (related instanceof Node && timelineEl.contains(related)) return; hideIndicator(); }); const handleTimelineDrop = async (path, beforePath) => { if (this.timelineDropBusy) return; if (beforePath && path === beforePath) return; const from = ordered.findIndex((e) => e.file.path === path); if (from < 0) return; const moved = ordered[from]; const nextOrdered = ordered.filter((e) => e.file.path !== path); const to = beforePath ? nextOrdered.findIndex((e) => e.file.path === beforePath) : -1; const insertAt = to >= 0 ? to : nextOrdered.length; nextOrdered.splice(insertAt, 0, moved); const targetField = profile.sortField || "\u65F6\u95F4"; const action = await askThreeChoices(this.app, this.plugin.t("timelineMoveTitle"), this.plugin.t("timelineMoveChoice", { field: targetField }), { cancel: this.plugin.t("timelineMoveCancel"), order: this.plugin.t("timelineMoveOrderOnly"), orderAndProp: this.plugin.t("timelineMoveOrderAndProp") }); if (action === "cancel") { new obsidian.Notice(this.plugin.t("timelineMoveNoChange")); return; } this.timelineDropBusy = true; try { this.recordUndoSnapshot(profile); ordered.splice(0, ordered.length, ...nextOrdered); this.saveOrder(profile.id, this.orderKey(profile), ordered.map((e) => e.file)); if (action === "order+prop") { const field = profile.sortField || "\u65F6\u95F4"; const current = getFrontmatterValues(this.app.metadataCache.getFileCache(moved.file), field)[0] || ""; const next = await askTextInput(this.app, this.plugin.t("timelineMoveOrderAndProp"), this.plugin.t("timelineUpdatePrompt", { field }), current, this.plugin.t("timelineUpdatePrompt", { field })); if (next != null) { const value = next.trim(); await this.app.fileManager.processFrontMatter(moved.file, (fm) => { if (value) fm[field] = value; else delete fm[field]; }); } new obsidian.Notice(this.plugin.t("timelineMoveDone")); await this.rerenderPreserveScroll(); } else { const cardEls = /* @__PURE__ */ new Map(); for (const el of timelineEl.querySelectorAll("section[data-sfv-path]")) { cardEls.set(el.getAttribute("data-sfv-path"), el); } for (const entry of ordered) { const el = cardEls.get(entry.file.path); if (el) timelineEl.appendChild(el); } new obsidian.Notice(this.plugin.t("timelineMoveDone")); } } finally { this.timelineDropBusy = false; } }; timelineEl.addEventListener("drop", (e) => { e.preventDefault(); hideIndicator(); const payload = e.dataTransfer?.getData("text/plain") || ""; if (!payload) return; const obj = JSON.parse(payload); if (obj.token && this.timelineDragToken && obj.token !== this.timelineDragToken) return; if (!obj.path || obj.from !== "timeline") return; const beforePath = dropBeforePath; void handleTimelineDrop(obj.path, beforePath); }); for (const { file, cache } of ordered) { const color = colorForValue(getFrontmatterValues(cache, profile.colorField)[0] || null, this.plugin.data.colorConfig) || this.plugin.data.colorConfig.fallbackColor; this.renderCard(timelineEl, file, cache, profile, color, true); const card = timelineEl.lastElementChild; card.setAttribute("data-sfv-path", file.path); card.draggable = true; card.addEventListener("dragstart", (e) => { this.timelineDragToken = newId(); card.classList.add("sfv-dragging"); if (e.dataTransfer) { e.dataTransfer.effectAllowed = "move"; e.dataTransfer.setDragImage(card, 28, 20); } e.dataTransfer?.setData("text/plain", JSON.stringify({ path: file.path, from: "timeline", token: this.timelineDragToken })); }); card.addEventListener("dragend", () => { card.classList.remove("sfv-dragging"); this.timelineDragToken = ""; hideIndicator(); }); card.addEventListener("dragover", (e) => e.preventDefault()); } return; } const grouped = /* @__PURE__ */ new Map(); const unsetLabel = this.plugin.t("boardUnset"); for (const entry of filtered) { const cols = this.getBoardFieldValues(entry.file, entry.cache, profile.boardField); const targets = cols.length ? cols : [unsetLabel]; for (const col of targets) { if (!grouped.has(col)) grouped.set(col, []); grouped.get(col).push(entry); } } const columns = uniqueSorted([...grouped.keys()]); const orderActions = container.createEl("div"); orderActions.addClass("sfv-order-actions"); orderActions.createEl("button", { text: this.plugin.t("saveOrder") }).addEventListener("click", () => { for (const col of columns) { const arr = grouped.get(col) || []; this.saveOrder(profile.id, this.orderKey(profile, col), arr.map((e) => e.file)); } new obsidian.Notice(this.plugin.t("saveOrderDone")); }); orderActions.createEl("button", { text: this.plugin.t("resetOrder") }).addEventListener("click", () => { this.recordUndoSnapshot(profile); for (const col of columns) this.clearOrder(profile.id, this.orderKey(profile, col)); new obsidian.Notice(this.plugin.t("resetOrderDone")); this.renderView(); }); orderActions.createEl("button", { text: this.plugin.t("undoLastAction") }).addEventListener("click", () => { void this.undoLastAction(profile); }); const boardEl = container.createEl("div"); boardEl.addClass("sfv-board"); const handleDrop = async (payload, to, beforePath) => { const fromArr = grouped.get(payload.from) || []; const toArr = grouped.get(to) || []; const idx = fromArr.findIndex((e) => e.file.path === payload.path); if (idx < 0) return; let action = "order"; let nextBoardValue = void 0; if (payload.from !== to && profile.boardField) { action = await askThreeChoices(this.app, this.plugin.t("boardMoveTitle"), this.plugin.t("boardMoveChoice", { field: profile.boardField }), { cancel: this.plugin.t("boardMoveCancel"), orderAndProp: this.plugin.t("boardMoveOrderAndProp") }); if (action === "cancel") { new obsidian.Notice(this.plugin.t("boardMoveNoChange")); return; } nextBoardValue = to === unsetLabel ? "" : to; } this.recordUndoSnapshot(profile); const removed = fromArr.splice(idx, 1)[0]; const pos = beforePath ? toArr.findIndex((e) => e.file.path === beforePath) : -1; const insertAt = pos >= 0 ? pos : toArr.length; toArr.splice(insertAt, 0, removed); grouped.set(payload.from, fromArr); grouped.set(to, toArr); this.saveOrder(profile.id, this.orderKey(profile, payload.from), fromArr.map((e) => e.file)); this.saveOrder(profile.id, this.orderKey(profile, to), toArr.map((e) => e.file)); if (payload.from !== to && profile.boardField && action === "order+prop") { this.pendingBoardFieldValueByPath[removed.file.path] = nextBoardValue; if (removed.cache?.frontmatter) { if (nextBoardValue) removed.cache.frontmatter[profile.boardField] = nextBoardValue; else delete removed.cache.frontmatter[profile.boardField]; } await this.app.fileManager.processFrontMatter(removed.file, (fm) => { if (nextBoardValue) fm[profile.boardField] = nextBoardValue; else delete fm[profile.boardField]; }); } new obsidian.Notice(this.plugin.t("boardMoveDone")); await this.rerenderPreserveScroll(); }; for (const col of columns) { const ordered = this.applyOrder(profile.id, this.orderKey(profile, col), grouped.get(col) || []); grouped.set(col, ordered); const colWrap = boardEl.createEl("section"); colWrap.addClass("sfv-board-col"); colWrap.createEl("strong", { text: `${col} (${ordered.length})` }); const cardsEl = colWrap.createEl("div"); cardsEl.addClass("sfv-board-cards"); const dropIndicator = cardsEl.createEl("div"); dropIndicator.addClass("sfv-drop-indicator"); let dropBeforePath = null; const hideDropIndicator = () => { dropIndicator.removeClass("sfv-visible"); dropBeforePath = null; }; const getNextCardPath = (cardEl) => { let next = cardEl.nextElementSibling; while (next) { if (next.matches?.("section[data-sfv-path]")) return next.getAttribute("data-sfv-path") || null; next = next.nextElementSibling; } return null; }; const getCardDropBeforePath = (cardEl, clientY) => { const rect = cardEl.getBoundingClientRect(); if (clientY < rect.top + rect.height / 2) return cardEl.getAttribute("data-sfv-path") || null; return getNextCardPath(cardEl); }; const updateDropIndicator = (clientY) => { const cards = Array.from(cardsEl.querySelectorAll("section[data-sfv-path]")).filter((el) => !el.classList.contains("sfv-dragging")); if (!cards.length) { dropBeforePath = null; cardsEl.appendChild(dropIndicator); dropIndicator.addClass("sfv-visible"); return; } let beforeCard = null; for (const cardEl of cards) { const rect = cardEl.getBoundingClientRect(); if (clientY < rect.top + rect.height / 2) { beforeCard = cardEl; break; } } dropBeforePath = beforeCard?.getAttribute("data-sfv-path") || null; if (beforeCard) cardsEl.insertBefore(dropIndicator, beforeCard); else cardsEl.appendChild(dropIndicator); dropIndicator.addClass("sfv-visible"); }; cardsEl.addEventListener("dragover", (e) => { e.preventDefault(); if (e.dataTransfer) e.dataTransfer.dropEffect = "move"; updateDropIndicator(e.clientY); }); cardsEl.addEventListener("dragleave", (e) => { const related = e.relatedTarget; if (related instanceof Node && cardsEl.contains(related)) return; hideDropIndicator(); }); cardsEl.addEventListener("drop", (e) => { e.preventDefault(); const beforePath = dropBeforePath; hideDropIndicator(); const payload = e.dataTransfer?.getData("text/plain") || ""; if (!payload) return; void handleDrop(JSON.parse(payload), col, beforePath); }); for (const { file, cache } of ordered) { const color = colorForValue(getFrontmatterValues(cache, profile.colorField)[0] || null, this.plugin.data.colorConfig) || this.plugin.data.colorConfig.fallbackColor; this.renderCard(cardsEl, file, cache, profile, color, false); const card = cardsEl.lastElementChild; card.setAttribute("data-sfv-path", file.path); card.draggable = true; card.addEventListener("dragstart", (e) => { card.classList.add("sfv-dragging"); e.dataTransfer?.setData("text/plain", JSON.stringify({ path: file.path, from: col })); }); card.addEventListener("dragend", () => { card.classList.remove("sfv-dragging"); hideDropIndicator(); }); card.addEventListener("dragover", (e) => { e.preventDefault(); updateDropIndicator(e.clientY); }); card.addEventListener("drop", (e) => { e.preventDefault(); e.stopPropagation(); const beforePath = getCardDropBeforePath(card, e.clientY); hideDropIndicator(); const payload = e.dataTransfer?.getData("text/plain") || ""; if (!payload) return; void handleDrop(JSON.parse(payload), col, beforePath); }); } } } onClose() { } }; var SmartFolderSettingTab = class extends obsidian.PluginSettingTab { constructor(app, plugin) { super(app, plugin); this.plugin = plugin; } display() { const { containerEl } = this; containerEl.empty(); containerEl.createEl("h2", { text: this.plugin.t("title") }); new obsidian.Setting(containerEl).setName(this.plugin.t("openBuilder")).addButton((b) => b.setButtonText(this.plugin.t("openBuilder")).onClick(() => new SetupModal(this.app, this.plugin).open())); new obsidian.Setting(containerEl).setName(this.plugin.t("openManager")).addButton((b) => b.setButtonText(this.plugin.t("openManager")).onClick(() => new ProfileManagerModal(this.app, this.plugin).open())); new obsidian.Setting(containerEl).setName(this.plugin.t("languageMode")).setDesc(this.plugin.t("languageModeDesc")).addDropdown((d) => { d.addOption("auto", this.plugin.t("languageAuto")); d.addOption("zh", this.plugin.t("languageZh")); d.addOption("en", this.plugin.t("languageEn")); d.setValue(this.plugin.data.languageMode); makeSelectSearchable(d.selectEl, this.plugin.t("search")); d.onChange(async (v) => { this.plugin.data.languageMode = v; await this.plugin.persist(); this.display(); }); }); containerEl.createEl("h3", { text: this.plugin.t("colorSettings") }); new obsidian.Setting(containerEl).setName(this.plugin.t("saturationBase")).setDesc("20-80").addSlider((s) => s.setLimits(20, 80, 1).setValue(this.plugin.data.colorConfig.saturationBase).setDynamicTooltip().onChange(async (v) => { this.plugin.data.colorConfig.saturationBase = v; await this.plugin.persist(); })); new obsidian.Setting(containerEl).setName(this.plugin.t("saturationRange")).setDesc("1-40").addSlider((s) => s.setLimits(1, 40, 1).setValue(this.plugin.data.colorConfig.saturationRange).setDynamicTooltip().onChange(async (v) => { this.plugin.data.colorConfig.saturationRange = v; await this.plugin.persist(); })); new obsidian.Setting(containerEl).setName(this.plugin.t("lightnessBase")).setDesc("20-70").addSlider((s) => s.setLimits(20, 70, 1).setValue(this.plugin.data.colorConfig.lightnessBase).setDynamicTooltip().onChange(async (v) => { this.plugin.data.colorConfig.lightnessBase = v; await this.plugin.persist(); })); new obsidian.Setting(containerEl).setName(this.plugin.t("lightnessRange")).setDesc("1-30").addSlider((s) => s.setLimits(1, 30, 1).setValue(this.plugin.data.colorConfig.lightnessRange).setDynamicTooltip().onChange(async (v) => { this.plugin.data.colorConfig.lightnessRange = v; await this.plugin.persist(); })); new obsidian.Setting(containerEl).setName(this.plugin.t("fallbackColor")).setDesc("CSS color value, e.g. #3b82f6 or var(--interactive-accent)").addText((t) => t.setValue(this.plugin.data.colorConfig.fallbackColor).onChange(async (v) => { this.plugin.data.colorConfig.fallbackColor = v.trim() || DEFAULT_COLOR_CONFIG.fallbackColor; await this.plugin.persist(); })).addButton((b) => b.setButtonText(this.plugin.t("resetDefault")).onClick(async () => { this.plugin.data.colorConfig = { ...DEFAULT_COLOR_CONFIG }; await this.plugin.persist(); this.display(); })); } }; var SmartFolderPlugin = class extends obsidian.Plugin { t(key, vars) { const mode = this.data?.languageMode || "auto"; const lang = mode === "auto" ? detectLanguage() : mode; return tpl(I18N[lang]?.[key] || key, vars); } getFolderOptions() { const set = /* @__PURE__ */ new Set(); for (const f of this.app.vault.getMarkdownFiles()) { if (f.parent?.path) set.add(f.parent.path); } return ["", ...uniqueSorted([...set])]; } getFilesByFolder(sourceFolder, excludedPath = "") { const files = this.app.vault.getMarkdownFiles().filter((f) => { const cache = this.app.metadataCache.getFileCache(f); return f.path !== excludedPath && !isLikelySmartFolderExportPath(f.path) && !isSmartFolderExportCache(cache); }); if (!sourceFolder) return files; const prefix = sourceFolder.replace(/\/$/, "") + "/"; return files.filter((f) => f.path.startsWith(prefix) || f.parent?.path === sourceFolder); } getTemplateFileOptions() { return this.app.vault.getMarkdownFiles().map((f) => f.path); } getFieldCandidates(sourceFolder, templateFile, excludedPath = "") { const template = templateFile ? this.app.vault.getAbstractFileByPath(templateFile) : null; const templateKeys = template instanceof obsidian.TFile ? getAllFrontmatterKeys([this.app.metadataCache.getFileCache(template)]) : []; const files = this.getFilesByFolder(sourceFolder, excludedPath); const caches = files.map((f) => this.app.metadataCache.getFileCache(f)); const allKeys = getAllFrontmatterKeys(caches); const common = allKeys.filter((k) => caches.every((c) => !c?.frontmatter || k in c.frontmatter)); return uniqueSorted([...templateKeys, ...common]); } getCurrentProfile() { if (this.draftProfile) return this.draftProfile; const id = this.data.lastActiveProfileId; return this.data.profiles.find((p) => p.id === id) || null; } isDraft(id) { return !!this.draftProfile && this.draftProfile.id === id; } setDraftProfile(profile) { this.draftProfile = { ...profile, displayFields: [...profile.displayFields || []], updatedAt: Date.now() }; } clearDraftProfile() { this.draftProfile = null; } async saveProfile(profile, setActive, askNameIfEmpty) { const next = { ...profile, displayFields: [...profile.displayFields || []], updatedAt: Date.now() }; if (!next.name && askNameIfEmpty) next.name = (await askTextInput(this.app, this.t("pageNamePrompt"), this.t("pageNamePrompt"), next.sourceFolder || "", "") || "").trim(); if (!next.name) next.name = next.sourceFolder || `Page-${(/* @__PURE__ */ new Date()).toISOString()}`; const idx = this.data.profiles.findIndex((p) => p.id === next.id); if (idx >= 0) { this.data.profiles[idx] = { ...this.data.profiles[idx], ...next }; new obsidian.Notice(this.t("pageUpdated")); } else { next.createdAt = next.createdAt || Date.now(); this.data.profiles.push(next); new obsidian.Notice(this.t("pageSaved")); } if (setActive) this.data.lastActiveProfileId = next.id; if (this.draftProfile?.id === next.id) this.draftProfile = null; await this.persist(); } normalizeExportProfile(raw) { if (!raw || typeof raw !== "object") return null; const profile = { id: String(raw.id || newId()), name: String(raw.name || ""), sourceFolder: String(raw.sourceFolder || ""), templateFile: String(raw.templateFile || ""), colorField: String(raw.colorField || ""), displayFields: Array.isArray(raw.displayFields) ? raw.displayFields.map((v) => String(v)).filter(Boolean) : [], sortField: String(raw.sortField || ""), sortOrder: raw.sortOrder === "asc" ? "asc" : "desc", viewMode: raw.viewMode === "board" ? "board" : "timeline", boardField: String(raw.boardField || ""), sourceNotePath: String(raw.sourceNotePath || ""), createdAt: typeof raw.createdAt === "number" ? raw.createdAt : Date.now(), updatedAt: Date.now() }; return profile; } async openProfileFromExport(raw, opts = {}) { const profile = this.normalizeExportProfile(raw); if (!profile) return false; profile.sourceNotePath = String(opts.sourcePath || profile.sourceNotePath || ""); await this.activateView({ profile, newLeaf: opts.newLeaf === true, persistLastActive: false, leaf: opts.leaf || null }); return true; } async importProfileFromExport(raw, opts = {}) { const profile = this.normalizeExportProfile(raw); if (!profile) return false; profile.sourceNotePath = String(opts.sourcePath || profile.sourceNotePath || ""); const existing = this.data.profiles.find((p) => p.id === profile.id) || this.data.profiles.find((p) => p.name === profile.name && p.sourceFolder === profile.sourceFolder); if (existing) { profile.id = existing.id; profile.createdAt = existing.createdAt || profile.createdAt; } await this.saveProfile(profile, true, false); await this.activateView({ profile, newLeaf: true }); return true; } async exportProfileToMarkdown(profile) { if (!profile) { new obsidian.Notice(this.t("noProfile")); return; } try { const folders = this.getFolderOptions(); const defaultFolder = this.app.vault.getAbstractFileByPath("00 Homepage") instanceof obsidian.TFolder ? "00 Homepage" : ""; const chosenFolder = await askFolderSelect(this.app, this, folders, defaultFolder); if (chosenFolder == null) return; const safeBase = (profile.name || profile.sourceFolder || "smart-folder-page").replace(/[\\/:*?"<>|]/g, "-").trim() || "smart-folder-page"; const folderPrefix = chosenFolder ? `${chosenFolder}/` : ""; let path = `${folderPrefix}${safeBase}.smart-folder-page.md`; let i = 1; while (this.app.vault.getAbstractFileByPath(path)) { path = `${folderPrefix}${safeBase}.smart-folder-page-${i}.md`; i += 1; } const payload = { ...profile, exportedAt: (/* @__PURE__ */ new Date()).toISOString() }; const content = [ "# Smart Folder \u9875\u9762\u5BFC\u51FA", "", "", "", "```smart-folder-page", JSON.stringify(payload, null, 2), "```", "" ].join("\n"); await this.app.vault.create(path, content); new obsidian.Notice(this.t("exportPageDone", { path })); } catch { new obsidian.Notice(this.t("exportPageFailed")); } } async onload() { await this.loadPluginData(); this.registerView(VIEW_TYPE, (leaf) => new SmartFolderView(leaf, this)); this.registerMarkdownCodeBlockProcessor("smart-folder-page", (source, el, ctx) => { el.empty(); let raw; try { raw = JSON.parse(source || "{}"); } catch { el.createEl("p", { text: this.t("exportBlockInvalid") }); return; } const profile = this.normalizeExportProfile(raw); if (!profile) { el.createEl("p", { text: this.t("exportBlockInvalid") }); return; } const wrap = el.createEl("div"); wrap.addClass("sfv-export-block"); wrap.createEl("strong", { text: this.t("exportBlockTitle") }); wrap.createEl("p", { text: `${this.t("exportBlockDesc")} ${profile.name || profile.sourceFolder}` }).addClass("sfv-export-desc"); const row = wrap.createEl("div"); row.addClass("sfv-export-row"); const openBtn = row.createEl("button", { text: this.t("exportBlockOpen") }); openBtn.addClass("mod-cta"); openBtn.addEventListener("click", () => { const currentLeaf = this.app.workspace.getMostRecentLeaf(); void this.openProfileFromExport(raw, { leaf: currentLeaf, sourcePath: ctx.sourcePath }).then((ok) => { if (!ok) new obsidian.Notice(this.t("exportBlockInvalid")); }); }); const importBtn = row.createEl("button", { text: this.t("exportBlockImport") }); importBtn.addEventListener("click", () => { void this.importProfileFromExport(raw, { sourcePath: ctx.sourcePath }).then((ok) => { if (!ok) new obsidian.Notice(this.t("exportBlockInvalid")); }); }); }); this.registerDomEvent(document, "pointerdown", (ev) => { const target = ev.target; if (!(target instanceof Element)) return; const openBoxes = document.querySelectorAll(".smart-folder-view-root details[open]"); for (const box of openBoxes) { if (box.contains(target)) continue; box.removeAttribute("open"); } }, true); this.registerDomEvent(document, "keydown", (ev) => { if (ev.key !== "Escape") return; const openBoxes = document.querySelectorAll(".smart-folder-view-root details[open]"); for (const box of openBoxes) box.removeAttribute("open"); }); this.addRibbonIcon("layout-grid", this.t("title"), () => { void askStartAction(this.app, this).then((action) => { if (action === "open") new ProfileManagerModal(this.app, this).open(); else if (action === "create") new SetupModal(this.app, this).open(); }); }); this.addCommand({ id: "create-page", name: this.t("openBuilder"), callback: () => new SetupModal(this.app, this).open() }); this.addCommand({ id: "create-page-2", name: this.t("openBuilder"), callback: () => new SetupModal(this.app, this).open() }); this.addCommand({ id: "open-manager", name: this.t("openManager"), callback: () => new ProfileManagerModal(this.app, this).open() }); this.addCommand({ id: "open-last", name: this.t("openLast"), callback: () => { void (async () => { if (!this.getCurrentProfile()) { new obsidian.Notice(this.t("noProfile")); new SetupModal(this.app, this).open(); return; } await this.activateView({ profile: this.getCurrentProfile(), newLeaf: true }); })(); } }); this.addCommand({ id: "export-page", name: this.t("exportPage"), callback: () => { void (async () => { const profile = this.getCurrentProfile(); if (!profile) { new obsidian.Notice(this.t("noProfile")); return; } await this.exportProfileToMarkdown(profile); })(); } }); this.addSettingTab(new SmartFolderSettingTab(this.app, this)); } async activateView(opts = {}) { const { profile = null, newLeaf = false, persistLastActive = true, leaf: preferredLeaf = null } = opts; const { workspace } = this.app; let leaf = preferredLeaf; if (!leaf && !newLeaf) leaf = workspace.getLeavesOfType(VIEW_TYPE)[0] || null; if (!leaf) { leaf = workspace.getLeaf("tab"); } if (leaf.view.getViewType?.() !== VIEW_TYPE) { await leaf.setViewState({ type: VIEW_TYPE, active: true }); } void workspace.revealLeaf(leaf); const view = leaf.view; if (view instanceof SmartFolderView) { view.setPinnedProfile(profile || null); if (persistLastActive && profile?.id) { this.data.lastActiveProfileId = profile.id; await this.persist(); } view.onOpen(); } } onunload() { } async loadPluginData() { const raw = await this.loadData(); this.data = { language: raw?.language === "zh" || raw?.language === "en" ? raw.language : detectLanguage(), languageMode: raw?.languageMode === "auto" || raw?.languageMode === "zh" || raw?.languageMode === "en" ? raw.languageMode : "auto", profiles: Array.isArray(raw?.profiles) ? raw.profiles : [], lastActiveProfileId: typeof raw?.lastActiveProfileId === "string" ? raw.lastActiveProfileId : "", colorConfig: { saturationBase: typeof raw?.colorConfig?.saturationBase === "number" ? clamp(raw.colorConfig.saturationBase, 20, 80) : DEFAULT_COLOR_CONFIG.saturationBase, saturationRange: typeof raw?.colorConfig?.saturationRange === "number" ? clamp(raw.colorConfig.saturationRange, 1, 40) : DEFAULT_COLOR_CONFIG.saturationRange, lightnessBase: typeof raw?.colorConfig?.lightnessBase === "number" ? clamp(raw.colorConfig.lightnessBase, 20, 70) : DEFAULT_COLOR_CONFIG.lightnessBase, lightnessRange: typeof raw?.colorConfig?.lightnessRange === "number" ? clamp(raw.colorConfig.lightnessRange, 1, 30) : DEFAULT_COLOR_CONFIG.lightnessRange, fallbackColor: typeof raw?.colorConfig?.fallbackColor === "string" && raw.colorConfig.fallbackColor.trim() ? raw.colorConfig.fallbackColor : DEFAULT_COLOR_CONFIG.fallbackColor } }; this.draftProfile = null; } async persist() { await this.saveData(this.data); } }; module.exports = SmartFolderPlugin;