2026-05-03 10:44:44 +00:00
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 = {
2026-05-03 09:49:29 +00:00
saturationBase : 55 ,
saturationRange : 20 ,
lightnessBase : 44 ,
lightnessRange : 14 ,
2026-05-03 10:44:44 +00:00
fallbackColor : "var(--interactive-accent)"
2026-05-03 09:49:29 +00:00
} ;
2026-05-03 10:44:44 +00:00
var I18N = {
2026-05-03 09:49:29 +00:00
zh : {
2026-05-03 10:44:44 +00:00
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"
2026-05-03 09:49:29 +00:00
} ,
en : {
2026-05-03 10:44:44 +00:00
title : "Smart Folder View" ,
2026-05-03 12:52:40 +00:00
openBuilder : "Create Smart Folder page" ,
openManager : "Manage saved pages" ,
openLast : "Open last page" ,
2026-05-03 10:44:44 +00:00
noProfile : "No saved pages yet. Please create one first." ,
2026-05-03 12:52:40 +00:00
setupTitle : "Configure page" ,
2026-05-03 10:44:44 +00:00
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)" ,
2026-05-03 12:52:40 +00:00
savePage : "Save page" ,
updatePage : "Update page" ,
openBuilderBtn : "Create page" ,
openManagerBtn : "Page manager" ,
2026-05-03 10:44:44 +00:00
startActionTitle : "Smart Folder" ,
startActionDesc : "Choose an action:" ,
2026-05-03 12:52:40 +00:00
startOpenPage : "Open page" ,
startNewPage : "Create page" ,
2026-05-03 10:44:44 +00:00
pageSaved : "Page saved" ,
pageUpdated : "Page updated" ,
saveFailed : "Failed to save page" ,
pageNamePrompt : "Enter page name" ,
2026-05-03 12:52:40 +00:00
managerTitle : "Saved pages" ,
2026-05-03 10:44:44 +00:00
open : "Open" ,
edit : "Edit" ,
delete : "Delete" ,
deleteConfirm : "Delete page: {{name}}?" ,
2026-05-03 09:49:29 +00:00
noFiles : 'No notes found in "{{folder}}".' ,
2026-05-03 10:44:44 +00:00
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" ,
2026-05-03 09:49:29 +00:00
moveAsk : 'Cross-column move: update {{field}} to "{{value}}"?\nOK=recommended, Cancel=manual input' ,
2026-05-03 10:44:44 +00:00
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."
}
2026-05-03 09:49:29 +00:00
} ;
function tpl ( text , vars ) {
if ( ! vars ) return text ;
2026-05-03 10:44:44 +00:00
return text . replace ( /\{\{(\w+)\}\}/g , ( _m , key ) => String ( vars [ key ] ? ? "" ) ) ;
2026-05-03 09:49:29 +00:00
}
function detectLanguage ( ) {
try {
2026-05-03 10:44:44 +00:00
const locale = window . moment ? . locale ? . ( ) || "" ;
if ( locale . toLowerCase ( ) . startsWith ( "zh" ) ) return "zh" ;
2026-05-03 12:52:40 +00:00
} catch {
2026-05-03 10:44:44 +00:00
}
return "en" ;
2026-05-03 09:49:29 +00:00
}
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 ++ ) {
2026-05-03 10:44:44 +00:00
h = ( h << 5 ) - h + str . charCodeAt ( i ) ;
2026-05-03 09:49:29 +00:00
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 ;
2026-05-03 10:44:44 +00:00
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 ) ;
2026-05-03 09:49:29 +00:00
return ` hsl( ${ h } ${ s } % ${ l } %) ` ;
}
function uniqueSorted ( items ) {
2026-05-03 10:44:44 +00:00
return [ ... new Set ( items ) ] . sort ( ( a , b ) => a . localeCompare ( b , "zh-Hans-CN" ) ) ;
2026-05-03 09:49:29 +00:00
}
function toFlatArray ( value ) {
if ( value == null ) return [ ] ;
2026-05-04 08:50:55 +00:00
if ( Array . isArray ( value ) ) return value . flatMap ( ( v ) => toFlatArray ( v ) ) ;
2026-05-04 13:50:42 +00:00
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 [ ] ;
2026-05-03 09:49:29 +00:00
}
function getFrontmatterValues ( cache , key ) {
if ( ! cache ? . frontmatter ) return [ ] ;
return toFlatArray ( cache . frontmatter [ key ] ) ;
}
function getAllFrontmatterKeys ( caches ) {
2026-05-03 10:44:44 +00:00
const keys = /* @__PURE__ */ new Set ( ) ;
2026-05-03 09:49:29 +00:00
for ( const cache of caches ) {
if ( cache ? . frontmatter ) {
for ( const key of Object . keys ( cache . frontmatter ) ) {
2026-05-03 10:44:44 +00:00
if ( key !== "position" ) keys . add ( key ) ;
2026-05-03 09:49:29 +00:00
}
}
}
return uniqueSorted ( [ ... keys ] ) ;
}
function stripFrontmatter ( text ) {
2026-05-03 10:44:44 +00:00
return text . replace ( /^---\n[\s\S]*?\n---\n?/ , "" ) . trim ( ) ;
2026-05-03 09:49:29 +00:00
}
function parseDateLike ( value ) {
2026-05-03 10:44:44 +00:00
const raw = String ( value || "" ) . trim ( ) ;
2026-05-03 09:49:29 +00:00
if ( ! raw ) return null ;
2026-06-04 10:46:05 +00:00
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 ) ;
}
2026-05-03 09:49:29 +00:00
return null ;
}
2026-06-04 10:46:05 +00:00
function isChronologicalSortField ( field ) {
const normalized = String ( field || "" ) . toLowerCase ( ) . replace ( /\s+/g , "" ) ;
return /时间|日期|发生于|年份|年代|date|time|year|created|updated/ . test ( normalized ) ;
}
function parseSortValue ( value , sortField = "" ) {
2026-05-03 10:44:44 +00:00
const raw = String ( value == null ? "" : value ) . trim ( ) ;
2026-06-04 10:46:05 +00:00
if ( isChronologicalSortField ( sortField ) ) {
const ts = parseDateLike ( raw ) ;
if ( ts != null ) return { kind : "number" , value : ts , raw } ;
}
2026-05-03 10:44:44 +00:00
const numericText = raw . replace ( /,/g , "" ) ;
if ( /^-?\d+(\.\d+)?$/ . test ( numericText ) ) return { kind : "number" , value : Number ( numericText ) , raw } ;
return { kind : "string" , value : raw , raw } ;
2026-05-03 09:49:29 +00:00
}
function clonePreset ( preset ) {
return {
2026-05-03 10:44:44 +00:00
name : String ( preset ? . name || "" ) ,
2026-05-03 09:49:29 +00:00
enabledFilters : Array . isArray ( preset ? . enabledFilters ) ? [ ... preset . enabledFilters ] : [ ] ,
selectedByAttr : JSON . parse ( JSON . stringify ( preset ? . selectedByAttr || { } ) ) ,
2026-05-03 10:44:44 +00:00
sortOrder : preset ? . sortOrder === "asc" ? "asc" : "desc" ,
2026-06-04 10:46:05 +00:00
viewMode : preset ? . viewMode === "board" ? "board" : "timeline" ,
boardField : typeof preset ? . boardField === "string" ? preset . boardField : ""
2026-05-03 09:49:29 +00:00
} ;
}
function clonePresetList ( presets ) {
return ( Array . isArray ( presets ) ? presets : [ ] ) . map ( clonePreset ) ;
}
function cloneSelectedByAttr ( state ) {
return JSON . parse ( JSON . stringify ( state || { } ) ) ;
}
2026-06-04 10:46:05 +00:00
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 ) ;
}
2026-05-03 09:49:29 +00:00
function makeSelectSearchable ( selectEl , placeholder ) {
2026-05-03 11:53:24 +00:00
void selectEl ;
void placeholder ;
2026-05-03 09:49:29 +00:00
}
2026-05-03 10:44:44 +00:00
var SetupModal = class extends obsidian . Modal {
2026-05-03 09:49:29 +00:00
constructor ( app , plugin , profile ) {
super ( app ) ;
this . plugin = plugin ;
this . hasPreviewedDraft = false ;
this . skipCloseSavePrompt = false ;
const folders = this . plugin . getFolderOptions ( ) ;
2026-05-03 10:44:44 +00:00
const sourceFolder = profile ? . sourceFolder || folders [ 0 ] || "" ;
const template = profile ? . templateFile || this . plugin . getTemplateFileOptions ( ) [ 0 ] || "" ;
2026-05-03 09:49:29 +00:00
this . draft = profile ? { ... profile , displayFields : [ ... profile . displayFields ] } : {
id : newId ( ) ,
2026-05-03 10:44:44 +00:00
name : "" ,
2026-05-03 09:49:29 +00:00
sourceFolder ,
templateFile : template ,
2026-05-03 10:44:44 +00:00
colorField : "" ,
2026-05-03 09:49:29 +00:00
displayFields : [ ] ,
2026-05-03 10:44:44 +00:00
sortField : "" ,
sortOrder : "desc" ,
viewMode : "timeline" ,
boardField : "\u72B6\u6001" ,
2026-05-03 09:49:29 +00:00
createdAt : Date . now ( ) ,
2026-05-03 10:44:44 +00:00
updatedAt : Date . now ( )
2026-05-03 09:49:29 +00:00
} ;
}
render ( ) {
const { contentEl } = this ;
contentEl . empty ( ) ;
2026-05-03 10:44:44 +00:00
contentEl . createEl ( "h2" , { text : this . plugin . t ( "setupTitle" ) } ) ;
contentEl . createEl ( "p" , { text : this . plugin . t ( "setupDesc" ) } ) ;
2026-05-03 09:49:29 +00:00
const folders = this . plugin . getFolderOptions ( ) ;
2026-05-03 10:44:44 +00:00
if ( ! folders . includes ( this . draft . sourceFolder ) ) this . draft . sourceFolder = folders [ 0 ] || "" ;
2026-05-03 09:49:29 +00:00
const templates = this . plugin . getTemplateFileOptions ( ) ;
2026-05-03 10:44:44 +00:00
if ( ! templates . includes ( this . draft . templateFile ) ) this . draft . templateFile = templates [ 0 ] || "" ;
2026-06-27 20:00:30 +00:00
const fieldOptions = this . plugin . getFieldCandidates ( this . draft . sourceFolder , this . draft . templateFile , this . draft . sourceNotePath ) ;
2026-05-03 10:44:44 +00:00
if ( ! fieldOptions . includes ( this . draft . colorField ) ) this . draft . colorField = fieldOptions [ 0 ] || "" ;
if ( ! fieldOptions . includes ( this . draft . sortField ) ) this . draft . sortField = fieldOptions [ 0 ] || "" ;
2026-05-03 09:49:29 +00:00
if ( ! fieldOptions . includes ( this . draft . boardField ) ) {
2026-05-03 10:44:44 +00:00
this . draft . boardField = fieldOptions . includes ( "\u72B6\u6001" ) ? "\u72B6\u6001" : fieldOptions [ 0 ] || "" ;
2026-05-03 09:49:29 +00:00
}
2026-05-03 10:44:44 +00:00
this . draft . displayFields = this . draft . displayFields . filter ( ( k ) => fieldOptions . includes ( k ) ) ;
2026-05-03 09:49:29 +00:00
if ( ! this . draft . displayFields . length ) this . draft . displayFields = fieldOptions . slice ( 0 , 4 ) ;
2026-05-03 10:44:44 +00:00
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 ( ) ;
2026-05-03 09:49:29 +00:00
} ) ;
2026-05-03 10:44:44 +00:00
} ) ;
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 ( ) ;
2026-05-03 09:49:29 +00:00
} ) ;
2026-05-03 10:44:44 +00:00
} ) ;
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 ;
2026-05-03 09:49:29 +00:00
} ) ;
2026-05-03 10:44:44 +00:00
} ) ;
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 ;
2026-05-03 09:49:29 +00:00
} ) ;
2026-05-03 10:44:44 +00:00
} ) ;
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 ;
2026-05-03 09:49:29 +00:00
} ) ;
2026-05-03 10:44:44 +00:00
} ) ;
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 ( ) ;
2026-05-03 09:49:29 +00:00
} ) ;
2026-05-03 10:44:44 +00:00
} ) ;
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 ;
2026-05-03 09:49:29 +00:00
} ) ;
2026-05-03 10:44:44 +00:00
} ) ;
const action = contentEl . createEl ( "div" ) ;
2026-05-03 11:49:14 +00:00
action . addClass ( "sfv-setup-action" ) ;
action . createEl ( "button" , { text : this . plugin . t ( "preview" ) } ) . addEventListener ( "click" , ( ) => {
2026-05-03 09:49:29 +00:00
this . plugin . setDraftProfile ( this . draft ) ;
2026-05-03 11:49:14 +00:00
void this . plugin . activateView ( ) ;
2026-05-03 09:49:29 +00:00
this . hasPreviewedDraft = true ;
} ) ;
2026-05-03 10:44:44 +00:00
const saveBtn = action . createEl ( "button" , { text : this . plugin . t ( "saveAndOpen" ) } ) ;
saveBtn . addClass ( "mod-cta" ) ;
2026-05-03 11:49:14 +00:00
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 ( ) ;
} ) ( ) ;
2026-05-03 09:49:29 +00:00
} ) ;
}
2026-05-03 10:44:44 +00:00
onOpen ( ) {
this . render ( ) ;
}
2026-05-03 11:49:14 +00:00
onClose ( ) {
2026-05-03 09:49:29 +00:00
this . contentEl . empty ( ) ;
2026-05-03 11:49:14 +00:00
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 } )
) ;
} ) ;
2026-05-03 09:49:29 +00:00
}
2026-05-03 10:44:44 +00:00
} ;
var TextInputModal = class extends obsidian . Modal {
2026-05-03 09:49:29 +00:00
constructor ( app , title , placeholder , initial , onSubmit , hint ) {
super ( app ) ;
this . titleText = title ;
this . hintText = hint ;
this . placeholderText = placeholder ;
this . initialValue = initial ;
this . onSubmitValue = onSubmit ;
2026-06-04 10:46:05 +00:00
this . resolved = false ;
}
submit ( value ) {
if ( this . resolved ) return ;
this . resolved = true ;
this . onSubmitValue ( value ) ;
2026-05-03 09:49:29 +00:00
}
onOpen ( ) {
const { contentEl } = this ;
contentEl . empty ( ) ;
2026-05-03 10:44:44 +00:00
contentEl . createEl ( "h3" , { text : this . titleText } ) ;
2026-05-03 11:49:14 +00:00
if ( this . hintText ) contentEl . createEl ( "p" , { text : this . hintText } ) . addClass ( "sfv-modal-hint" ) ;
2026-05-03 10:44:44 +00:00
const input = contentEl . createEl ( "input" ) ;
input . type = "text" ;
2026-05-03 09:49:29 +00:00
input . placeholder = this . placeholderText ;
input . value = this . initialValue ;
2026-05-03 11:49:14 +00:00
input . addClass ( "sfv-modal-input" ) ;
2026-05-03 10:44:44 +00:00
const action = contentEl . createEl ( "div" ) ;
2026-05-03 11:49:14 +00:00
action . addClass ( "sfv-modal-action" ) ;
2026-05-03 10:44:44 +00:00
action . createEl ( "button" , { text : "Cancel" } ) . addEventListener ( "click" , ( ) => {
2026-06-04 10:46:05 +00:00
this . submit ( null ) ;
2026-05-03 09:49:29 +00:00
this . close ( ) ;
} ) ;
2026-05-03 10:44:44 +00:00
const okBtn = action . createEl ( "button" , { text : "OK" } ) ;
okBtn . addClass ( "mod-cta" ) ;
okBtn . addEventListener ( "click" , ( ) => {
2026-06-04 10:46:05 +00:00
this . submit ( input . value . trim ( ) ) ;
2026-05-03 09:49:29 +00:00
this . close ( ) ;
} ) ;
2026-05-03 10:44:44 +00:00
input . addEventListener ( "keydown" , ( e ) => {
if ( e . key === "Enter" ) {
2026-05-03 09:49:29 +00:00
e . preventDefault ( ) ;
2026-06-04 10:46:05 +00:00
this . submit ( input . value . trim ( ) ) ;
2026-05-03 09:49:29 +00:00
this . close ( ) ;
}
} ) ;
window . setTimeout ( ( ) => input . focus ( ) , 0 ) ;
}
onClose ( ) {
2026-06-04 10:46:05 +00:00
this . submit ( null ) ;
2026-05-03 09:49:29 +00:00
this . contentEl . empty ( ) ;
}
2026-05-03 10:44:44 +00:00
} ;
2026-05-03 09:49:29 +00:00
function askTextInput ( app , title , placeholder , initial , hint ) {
2026-05-03 10:44:44 +00:00
return new Promise ( ( resolve ) => {
2026-05-03 09:49:29 +00:00
new TextInputModal ( app , title , placeholder , initial , resolve , hint ) . open ( ) ;
} ) ;
}
2026-05-03 10:44:44 +00:00
var ThreeChoiceModal = class extends obsidian . Modal {
2026-05-03 09:49:29 +00:00
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 ( ) ;
2026-05-03 10:44:44 +00:00
contentEl . createEl ( "h3" , { text : this . titleText } ) ;
2026-05-03 11:49:14 +00:00
contentEl . createEl ( "p" , { text : this . messageText } ) . addClass ( "sfv-modal-hint" ) ;
2026-05-03 10:44:44 +00:00
const action = contentEl . createEl ( "div" ) ;
2026-05-03 11:49:14 +00:00
action . addClass ( "sfv-modal-action" ) ;
2026-05-03 09:49:29 +00:00
const addChoice = ( key , label , isCta = false ) => {
if ( ! label ) return ;
2026-05-03 10:44:44 +00:00
const btn = action . createEl ( "button" , { text : label } ) ;
if ( isCta ) btn . addClass ( "mod-cta" ) ;
btn . addEventListener ( "click" , ( ) => {
2026-05-03 09:49:29 +00:00
this . onSubmitValue ( key ) ;
this . close ( ) ;
} ) ;
} ;
2026-05-03 10:44:44 +00:00
addChoice ( "cancel" , this . labels . cancel ) ;
addChoice ( "order" , this . labels . order ) ;
addChoice ( "order+prop" , this . labels . orderAndProp , true ) ;
2026-05-03 09:49:29 +00:00
}
onClose ( ) {
this . contentEl . empty ( ) ;
}
2026-05-03 10:44:44 +00:00
} ;
2026-05-03 09:49:29 +00:00
function askThreeChoices ( app , title , message , labels ) {
2026-05-03 10:44:44 +00:00
return new Promise ( ( resolve ) => {
2026-05-03 09:49:29 +00:00
new ThreeChoiceModal ( app , title , message , labels , resolve ) . open ( ) ;
} ) ;
}
2026-05-03 10:44:44 +00:00
var StartActionModal = class extends obsidian . Modal {
2026-05-03 09:49:29 +00:00
constructor ( app , plugin , onSubmit ) {
super ( app ) ;
this . plugin = plugin ;
this . onSubmitValue = onSubmit ;
}
onOpen ( ) {
const { contentEl } = this ;
contentEl . empty ( ) ;
2026-05-03 10:44:44 +00:00
contentEl . createEl ( "h3" , { text : this . plugin . t ( "startActionTitle" ) } ) ;
2026-05-03 11:49:14 +00:00
contentEl . createEl ( "p" , { text : this . plugin . t ( "startActionDesc" ) } ) . addClass ( "sfv-modal-hint" ) ;
2026-05-03 10:44:44 +00:00
const action = contentEl . createEl ( "div" ) ;
2026-05-03 11:49:14 +00:00
action . addClass ( "sfv-modal-action" ) ;
2026-05-03 10:44:44 +00:00
action . createEl ( "button" , { text : this . plugin . t ( "startOpenPage" ) } ) . addEventListener ( "click" , ( ) => {
this . onSubmitValue ( "open" ) ;
2026-05-03 09:49:29 +00:00
this . close ( ) ;
} ) ;
2026-05-03 10:44:44 +00:00
const createBtn = action . createEl ( "button" , { text : this . plugin . t ( "startNewPage" ) } ) ;
createBtn . addClass ( "mod-cta" ) ;
createBtn . addEventListener ( "click" , ( ) => {
this . onSubmitValue ( "create" ) ;
2026-05-03 09:49:29 +00:00
this . close ( ) ;
} ) ;
}
onClose ( ) {
this . contentEl . empty ( ) ;
}
2026-05-03 10:44:44 +00:00
} ;
2026-05-03 09:49:29 +00:00
function askStartAction ( app , plugin ) {
2026-05-03 10:44:44 +00:00
return new Promise ( ( resolve ) => {
2026-05-03 09:49:29 +00:00
new StartActionModal ( app , plugin , resolve ) . open ( ) ;
} ) ;
}
2026-05-03 10:44:44 +00:00
var FolderSelectModal = class extends obsidian . Modal {
2026-05-03 09:49:29 +00:00
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 ( ) ;
2026-05-03 10:44:44 +00:00
contentEl . createEl ( "h3" , { text : this . plugin . t ( "exportFolderTitle" ) } ) ;
2026-05-03 11:49:14 +00:00
contentEl . createEl ( "p" , { text : this . plugin . t ( "exportFolderDesc" ) } ) . addClass ( "sfv-modal-hint" ) ;
2026-05-03 10:44:44 +00:00
const select = contentEl . createEl ( "select" ) ;
2026-05-03 11:49:14 +00:00
select . addClass ( "sfv-modal-select" ) ;
2026-05-03 09:49:29 +00:00
for ( const f of this . folders ) {
2026-05-03 10:44:44 +00:00
select . createEl ( "option" , { value : f , text : f || this . plugin . t ( "rootFolder" ) } ) ;
2026-05-03 09:49:29 +00:00
}
select . value = this . initialValue ;
2026-05-03 10:44:44 +00:00
makeSelectSearchable ( select , this . plugin . t ( "search" ) ) ;
const action = contentEl . createEl ( "div" ) ;
2026-05-03 11:49:14 +00:00
action . addClass ( "sfv-modal-action" ) ;
2026-05-03 10:44:44 +00:00
action . createEl ( "button" , { text : "Cancel" } ) . addEventListener ( "click" , ( ) => {
2026-05-03 09:49:29 +00:00
this . onSubmitValue ( null ) ;
this . close ( ) ;
} ) ;
2026-05-03 10:44:44 +00:00
const okBtn = action . createEl ( "button" , { text : this . plugin . t ( "exportFolderConfirm" ) } ) ;
okBtn . addClass ( "mod-cta" ) ;
okBtn . addEventListener ( "click" , ( ) => {
2026-05-03 09:49:29 +00:00
this . onSubmitValue ( select . value ) ;
this . close ( ) ;
} ) ;
}
onClose ( ) {
this . contentEl . empty ( ) ;
}
2026-05-03 10:44:44 +00:00
} ;
2026-05-03 09:49:29 +00:00
function askFolderSelect ( app , plugin , folders , initialValue ) {
2026-05-03 10:44:44 +00:00
return new Promise ( ( resolve ) => {
2026-05-03 09:49:29 +00:00
new FolderSelectModal ( app , plugin , folders , initialValue , resolve ) . open ( ) ;
} ) ;
}
2026-05-03 11:49:14 +00:00
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 ( ) ;
} ) ;
}
2026-05-03 10:44:44 +00:00
var ProfileManagerModal = class extends obsidian . Modal {
2026-05-03 09:49:29 +00:00
constructor ( app , plugin ) {
super ( app ) ;
this . plugin = plugin ;
}
onOpen ( ) {
const { contentEl } = this ;
contentEl . empty ( ) ;
2026-05-03 10:44:44 +00:00
contentEl . createEl ( "h2" , { text : this . plugin . t ( "managerTitle" ) } ) ;
2026-05-03 09:49:29 +00:00
for ( const profile of this . plugin . data . profiles ) {
const s = new obsidian . Setting ( contentEl ) . setName ( profile . name || profile . sourceFolder ) . setDesc ( profile . sourceFolder ) ;
2026-05-03 12:52:40 +00:00
s . addButton ( ( b ) => b . setButtonText ( this . plugin . t ( "open" ) ) . onClick ( ( ) => {
2026-05-03 11:49:14 +00:00
void ( async ( ) => {
this . plugin . clearDraftProfile ( ) ;
this . plugin . data . lastActiveProfileId = profile . id ;
await this . plugin . persist ( ) ;
await this . plugin . activateView ( { profile , newLeaf : true } ) ;
this . close ( ) ;
} ) ( ) ;
2026-05-03 09:49:29 +00:00
} ) ) ;
2026-05-03 10:44:44 +00:00
s . addButton ( ( b ) => b . setButtonText ( this . plugin . t ( "edit" ) ) . onClick ( ( ) => {
2026-05-03 09:49:29 +00:00
new SetupModal ( this . app , this . plugin , profile ) . open ( ) ;
} ) ) ;
2026-05-03 11:49:14 +00:00
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 ( ) ;
} ) ;
2026-05-03 09:49:29 +00:00
} ) ) ;
}
}
2026-05-03 10:44:44 +00:00
onClose ( ) {
this . contentEl . empty ( ) ;
}
} ;
var SmartFolderView = class extends obsidian . ItemView {
2026-05-03 09:49:29 +00:00
constructor ( leaf , plugin ) {
super ( leaf ) ;
this . plugin = plugin ;
this . pinnedProfile = null ;
2026-05-03 10:44:44 +00:00
this . stateOwnerId = "" ;
this . profileDefaultsKey = "" ;
2026-05-03 09:49:29 +00:00
this . timelineDropBusy = false ;
2026-05-03 10:44:44 +00:00
this . timelineDragToken = "" ;
2026-05-03 09:49:29 +00:00
this . pendingBoardFieldValueByPath = { } ;
this . lastUndoSnapshot = null ;
}
2026-05-03 10:44:44 +00:00
getViewType ( ) {
return VIEW _TYPE ;
}
2026-05-03 09:49:29 +00:00
getDisplayText ( ) {
const p = this . pinnedProfile || this . plugin . getCurrentProfile ( ) ;
2026-05-03 10:44:44 +00:00
return p ? . name || p ? . sourceFolder || this . plugin . t ( "title" ) ;
}
getIcon ( ) {
return "layout-grid" ;
2026-05-03 09:49:29 +00:00
}
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 {
2026-05-03 10:44:44 +00:00
this . pinnedProfile = { ... profile , displayFields : [ ... profile . displayFields || [ ] ] } ;
2026-05-03 09:49:29 +00:00
}
2026-05-03 10:44:44 +00:00
this . stateOwnerId = "" ;
this . profileDefaultsKey = "" ;
2026-05-03 09:49:29 +00:00
this . lastUndoSnapshot = null ;
this . updateLeafTitle ( ) ;
}
getRenderProfile ( ) {
if ( ! this . pinnedProfile ) return this . plugin . getCurrentProfile ( ) ;
2026-05-03 10:44:44 +00:00
const latest = this . plugin . data . profiles . find ( ( p ) => p . id === this . pinnedProfile . id ) || this . pinnedProfile ;
2026-06-27 20:00:30 +00:00
this . pinnedProfile = {
... latest ,
sourceNotePath : this . pinnedProfile . sourceNotePath || latest . sourceNotePath || "" ,
displayFields : [ ... latest . displayFields || [ ] ]
} ;
2026-05-03 09:49:29 +00:00
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 {
2026-05-03 11:49:14 +00:00
const saved = this . app . loadLocalStorage ( FILTER _STORAGE _PREFIX + ownerId ) || { } ;
2026-05-03 09:49:29 +00:00
this . isFreshFilterState = ! saved || Object . keys ( saved ) . length === 0 ;
2026-05-03 10:44:44 +00:00
const savedDefaultsKey = typeof saved . profileDefaultsKey === "string" ? saved . profileDefaultsKey : "" ;
2026-05-03 09:49:29 +00:00
const defaultsChanged = ! ! savedDefaultsKey && savedDefaultsKey !== this . profileDefaultsKey ;
this . filterState = {
enabledFilters : Array . isArray ( saved . enabledFilters ) ? saved . enabledFilters : [ ] ,
2026-05-03 10:44:44 +00:00
selectedByAttr : saved . selectedByAttr && typeof saved . selectedByAttr === "object" ? saved . selectedByAttr : { } ,
2026-06-04 10:46:05 +00:00
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
2026-05-03 09:49:29 +00:00
} ;
} catch {
this . isFreshFilterState = true ;
2026-06-04 10:46:05 +00:00
this . filterState = { enabledFilters : [ ] , selectedByAttr : { } , sortOrder : defaultSort , viewMode : defaultViewMode , presetName : "" , boardField : null } ;
2026-05-03 09:49:29 +00:00
}
}
saveState ( ) {
2026-05-03 11:49:14 +00:00
this . app . saveLocalStorage ( FILTER _STORAGE _PREFIX + this . stateOwnerId , {
2026-05-03 09:49:29 +00:00
... this . filterState ,
2026-05-03 10:44:44 +00:00
profileDefaultsKey : this . profileDefaultsKey
2026-05-03 11:49:14 +00:00
} ) ;
2026-05-03 09:49:29 +00:00
}
loadPresets ( ) {
try {
2026-05-03 11:49:14 +00:00
const parsed = this . app . loadLocalStorage ( FILTER _PRESET _STORAGE _PREFIX + this . stateOwnerId ) ;
2026-05-03 09:49:29 +00:00
if ( ! Array . isArray ( parsed ) ) return [ ] ;
return clonePresetList ( parsed ) ;
2026-05-03 10:44:44 +00:00
} catch {
return [ ] ;
}
2026-05-03 09:49:29 +00:00
}
savePresets ( presets ) {
2026-05-03 11:49:14 +00:00
this . app . saveLocalStorage ( FILTER _PRESET _STORAGE _PREFIX + this . stateOwnerId , clonePresetList ( presets ) ) ;
2026-05-03 09:49:29 +00:00
}
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 ) {
2026-05-03 10:44:44 +00:00
return ` ${ this . contextKey ( profile ) } : ${ column || "timeline" } ` ;
2026-05-03 09:49:29 +00:00
}
loadOrderStore ( ownerId ) {
try {
2026-05-03 11:49:14 +00:00
const parsed = this . app . loadLocalStorage ( MANUAL _ORDER _STORAGE _PREFIX + ownerId ) ;
return parsed && typeof parsed === "object" && ! Array . isArray ( parsed ) ? parsed : { } ;
2026-05-03 10:44:44 +00:00
} catch {
return { } ;
}
2026-05-03 09:49:29 +00:00
}
saveOrderStore ( ownerId , store ) {
2026-05-03 11:49:14 +00:00
this . app . saveLocalStorage ( MANUAL _ORDER _STORAGE _PREFIX + ownerId , store ) ;
2026-05-03 09:49:29 +00:00
}
applyOrder ( ownerId , key , items ) {
const store = this . loadOrderStore ( ownerId ) ;
const order = store [ key ] || [ ] ;
if ( ! order . length ) return items ;
2026-05-03 10:44:44 +00:00
const rank = /* @__PURE__ */ new Map ( ) ;
2026-05-03 09:49:29 +00:00
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 ) ;
2026-05-03 10:44:44 +00:00
store [ key ] = [ ... new Set ( files . map ( ( f ) => f . path ) ) ] ;
2026-05-03 09:49:29 +00:00
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 ) ,
2026-05-03 10:44:44 +00:00
pendingBoardFieldValueByPath : { ... this . pendingBoardFieldValueByPath }
2026-05-03 09:49:29 +00:00
} ;
}
async undoLastAction ( profile ) {
if ( ! this . lastUndoSnapshot || this . lastUndoSnapshot . ownerId !== profile . id ) {
2026-05-03 10:44:44 +00:00
new obsidian . Notice ( this . plugin . t ( "undoNone" ) ) ;
2026-05-03 09:49:29 +00:00
return ;
}
this . saveOrderStore ( profile . id , this . lastUndoSnapshot . orderStore || { } ) ;
2026-05-03 10:44:44 +00:00
this . pendingBoardFieldValueByPath = { ... this . lastUndoSnapshot . pendingBoardFieldValueByPath || { } } ;
2026-05-03 09:49:29 +00:00
this . lastUndoSnapshot = null ;
await this . rerenderPreserveScroll ( ) ;
2026-05-03 10:44:44 +00:00
new obsidian . Notice ( this . plugin . t ( "undoDone" ) ) ;
2026-05-03 09:49:29 +00:00
}
2026-05-03 11:49:14 +00:00
onOpen ( ) {
this . renderView ( ) ;
2026-05-03 09:49:29 +00:00
}
async rerenderPreserveScroll ( ) {
const container = this . containerEl . children [ 1 ] ;
const scrollTop = container ? . scrollTop || 0 ;
2026-05-03 12:52:40 +00:00
this . renderView ( ) ;
2026-05-03 09:49:29 +00:00
const nextContainer = this . containerEl . children [ 1 ] ;
2026-05-03 10:44:44 +00:00
await new Promise ( ( resolve ) => window . requestAnimationFrame ( ( ) => window . requestAnimationFrame ( resolve ) ) ) ;
2026-05-03 09:49:29 +00:00
if ( nextContainer ) nextContainer . scrollTop = scrollTop ;
}
async askAndMaybeUpdateField ( file , field , targetColumn , unsetLabel ) {
2026-05-03 10:44:44 +00:00
const suggested = targetColumn === unsetLabel ? "" : targetColumn ;
2026-05-03 11:49:14 +00:00
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 , "" ) ;
2026-05-03 10:44:44 +00:00
if ( finalValue == null ) return void 0 ;
2026-05-03 09:49:29 +00:00
const value = finalValue . trim ( ) ;
try {
2026-05-03 10:44:44 +00:00
await this . app . fileManager . processFrontMatter ( file , ( fm ) => {
2026-05-03 09:49:29 +00:00
if ( value ) fm [ field ] = value ;
else delete fm [ field ] ;
} ) ;
2026-05-03 10:44:44 +00:00
new obsidian . Notice ( this . plugin . t ( "moveDone" , { field , value : value || this . plugin . t ( "emptyValue" ) } ) ) ;
2026-05-03 09:49:29 +00:00
return value ;
} catch {
2026-05-03 10:44:44 +00:00
new obsidian . Notice ( this . plugin . t ( "moveFail" ) ) ;
return void 0 ;
2026-05-03 09:49:29 +00:00
}
}
getBoardFieldValue ( file , cache , boardField ) {
if ( Object . prototype . hasOwnProperty . call ( this . pendingBoardFieldValueByPath , file . path ) ) {
2026-05-03 10:44:44 +00:00
return this . pendingBoardFieldValueByPath [ file . path ] || "" ;
2026-05-03 09:49:29 +00:00
}
2026-05-03 10:44:44 +00:00
return getFrontmatterValues ( cache , boardField ) [ 0 ] || "" ;
2026-05-03 09:49:29 +00:00
}
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 ;
2026-05-03 10:44:44 +00:00
const pending = this . pendingBoardFieldValueByPath [ file . path ] || "" ;
const actual = getFrontmatterValues ( cache , boardField ) [ 0 ] || "" ;
2026-05-03 09:49:29 +00:00
if ( pending === actual ) delete this . pendingBoardFieldValueByPath [ file . path ] ;
}
renderCard ( parent , file , cache , profile , color , showDot ) {
2026-05-03 10:44:44 +00:00
const card = parent . createEl ( "section" ) ;
2026-05-03 11:49:14 +00:00
card . addClass ( "sfv-card" ) ;
2026-05-03 11:54:56 +00:00
card . setCssProps ( { "--sfv-color" : color } ) ;
2026-05-03 09:49:29 +00:00
if ( showDot ) {
2026-05-03 11:49:14 +00:00
card . createEl ( "span" ) . addClass ( "sfv-card-dot" ) ;
2026-05-03 09:49:29 +00:00
}
2026-05-03 10:44:44 +00:00
const top = card . createEl ( "div" ) ;
2026-05-03 11:49:14 +00:00
top . addClass ( "sfv-card-top" ) ;
2026-05-03 10:44:44 +00:00
const meta = top . createEl ( "span" ) ;
meta . setText ( profile . displayFields . map ( ( k ) => ` ${ k } : ${ getFrontmatterValues ( cache , k ) . join ( "\u3001" ) || "-" } ` ) . join ( " | " ) ) ;
2026-05-03 11:49:14 +00:00
meta . addClass ( "sfv-card-meta" ) ;
2026-05-03 10:44:44 +00:00
const link = top . createEl ( "a" ) ;
2026-05-03 09:49:29 +00:00
link . setText ( file . basename ) ;
2026-05-03 10:44:44 +00:00
link . className = "internal-link" ;
link . setAttribute ( "data-href" , file . path ) ;
link . setAttribute ( "href" , file . path ) ;
2026-05-03 11:49:14 +00:00
link . addClass ( "sfv-card-link" ) ;
2026-05-03 09:49:29 +00:00
const triggerHoverPreview = ( e ) => {
2026-05-03 10:44:44 +00:00
this . app . workspace ? . trigger ? . ( "hover-link" , {
2026-05-03 09:49:29 +00:00
event : e ,
source : VIEW _TYPE ,
hoverParent : card ,
targetEl : link ,
2026-05-03 10:44:44 +00:00
linktext : file . path
2026-05-03 09:49:29 +00:00
} ) ;
} ;
2026-05-03 10:44:44 +00:00
link . addEventListener ( "mouseenter" , triggerHoverPreview ) ;
link . addEventListener ( "mousemove" , triggerHoverPreview ) ;
link . addEventListener ( "mouseover" , triggerHoverPreview ) ;
link . addEventListener ( "pointerenter" , triggerHoverPreview ) ;
link . addEventListener ( "pointermove" , triggerHoverPreview ) ;
link . addEventListener ( "click" , ( e ) => {
2026-05-03 09:49:29 +00:00
e . preventDefault ( ) ;
2026-05-04 00:29:43 +00:00
void this . app . workspace . openLinkText ( file . path , file . path , e . ctrlKey || e . metaKey ) ;
2026-05-03 09:49:29 +00:00
} ) ;
const triggerHoverPreviewForTarget = ( e , targetEl ) => {
2026-05-03 10:44:44 +00:00
const linktext = targetEl . getAttribute ( "data-href" ) || targetEl . getAttribute ( "href" ) || targetEl . textContent ? . trim ( ) || "" ;
2026-05-03 09:49:29 +00:00
if ( ! linktext ) return ;
2026-05-03 10:44:44 +00:00
this . app . workspace ? . trigger ? . ( "hover-link" , {
2026-05-03 09:49:29 +00:00
event : e ,
source : VIEW _TYPE ,
hoverParent : card ,
targetEl ,
2026-05-03 10:44:44 +00:00
linktext
2026-05-03 09:49:29 +00:00
} ) ;
} ;
const getInternalLinkElement = ( target ) => {
2026-05-03 10:44:44 +00:00
if ( target instanceof Element ) return target . closest ( ".internal-link" ) ;
if ( target instanceof Node && target . parentElement ) return target . parentElement . closest ( ".internal-link" ) ;
2026-05-03 09:49:29 +00:00
return null ;
} ;
2026-05-03 10:44:44 +00:00
const bodyEl = card . createEl ( "div" ) ;
bodyEl . addClass ( "sfv-card-body" ) ;
bodyEl . addEventListener ( "mouseover" , ( e ) => {
2026-05-03 09:49:29 +00:00
const el = getInternalLinkElement ( e . target ) ;
if ( ! el ) return ;
triggerHoverPreviewForTarget ( e , el ) ;
} , true ) ;
2026-05-03 10:44:44 +00:00
bodyEl . addEventListener ( "pointerover" , ( e ) => {
2026-05-03 09:49:29 +00:00
const el = getInternalLinkElement ( e . target ) ;
if ( ! el ) return ;
triggerHoverPreviewForTarget ( e , el ) ;
} , true ) ;
2026-05-03 10:44:44 +00:00
bodyEl . addEventListener ( "mousemove" , ( e ) => {
2026-05-03 09:49:29 +00:00
const el = getInternalLinkElement ( e . target ) ;
if ( ! el ) return ;
triggerHoverPreviewForTarget ( e , el ) ;
} , true ) ;
2026-05-03 10:44:44 +00:00
bodyEl . addEventListener ( "pointermove" , ( e ) => {
2026-05-03 09:49:29 +00:00
const el = getInternalLinkElement ( e . target ) ;
if ( ! el ) return ;
triggerHoverPreviewForTarget ( e , el ) ;
} , true ) ;
2026-05-03 10:44:44 +00:00
bodyEl . addEventListener ( "click" , ( e ) => {
2026-05-03 09:49:29 +00:00
const el = getInternalLinkElement ( e . target ) ;
if ( ! el ) return ;
2026-05-03 10:44:44 +00:00
const href = el . getAttribute ( "data-href" ) || el . getAttribute ( "href" ) ;
2026-05-03 09:49:29 +00:00
if ( ! href ) return ;
e . preventDefault ( ) ;
2026-05-04 00:29:43 +00:00
void this . app . workspace . openLinkText ( href , file . path , e . ctrlKey || e . metaKey ) ;
2026-05-03 09:49:29 +00:00
} , true ) ;
2026-05-03 12:52:40 +00:00
void this . app . vault . read ( file ) . then ( ( raw ) => {
2026-05-03 10:44:44 +00:00
const body = stripFrontmatter ( raw ) || "\uFF08\u65E0\u6B63\u6587\uFF09" ;
2026-05-04 00:29:43 +00:00
void obsidian . MarkdownRenderer . render ( this . app , body , bodyEl , file . path , this ) ;
2026-05-03 10:44:44 +00:00
} ) . catch ( ( ) => bodyEl . setText ( "\uFF08\u6B63\u6587\u8BFB\u53D6\u5931\u8D25\uFF09" ) ) ;
2026-05-03 09:49:29 +00:00
}
buildDropdown ( container , label , options , selectedValues , onChange , onCommit ) {
2026-05-03 10:44:44 +00:00
const wrap = container . createEl ( "div" ) ;
2026-05-03 11:49:14 +00:00
wrap . addClass ( "sfv-dd-wrap" ) ;
wrap . createEl ( "span" , { text : label } ) . addClass ( "sfv-dd-label" ) ;
2026-05-03 10:44:44 +00:00
const box = wrap . createEl ( "details" ) ;
2026-05-03 11:49:14 +00:00
box . addClass ( "sfv-dd-box" ) ;
2026-05-03 10:44:44 +00:00
const summary = box . createEl ( "summary" ) ;
2026-05-03 11:49:14 +00:00
summary . addClass ( "sfv-dd-summary" ) ;
2026-05-03 09:49:29 +00:00
const initialValues = [ ... selectedValues ] ;
let currentValues = [ ... initialValues ] ;
2026-05-03 10:44:44 +00:00
const summarySpan = summary . createEl ( "span" ) ;
2026-05-03 11:49:14 +00:00
summarySpan . addClass ( "sfv-dd-summary-text" ) ;
2026-05-03 10:44:44 +00:00
const toDisplayText = ( v ) => v === EMPTY _OPTION _VALUE ? this . plugin . t ( "emptyValue" ) : v ;
2026-05-03 09:49:29 +00:00
const refreshSummary = ( ) => {
const isAll = currentValues . length === options . length && options . length > 0 ;
2026-05-03 10:44:44 +00:00
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 ;
2026-05-03 09:49:29 +00:00
summarySpan . setText ( short ) ;
2026-05-03 10:44:44 +00:00
summarySpan . title = isAll ? this . plugin . t ( "selectAll" ) : full ;
2026-05-03 09:49:29 +00:00
} ;
refreshSummary ( ) ;
2026-05-03 10:44:44 +00:00
const panel = box . createEl ( "div" ) ;
2026-05-03 11:49:14 +00:00
panel . addClass ( "sfv-dd-panel" ) ;
2026-05-03 10:44:44 +00:00
const actionBar = panel . createEl ( "div" ) ;
2026-05-03 11:49:14 +00:00
actionBar . addClass ( "sfv-dd-action-bar" ) ;
2026-05-03 10:44:44 +00:00
const allBtn = actionBar . createEl ( "button" , { text : this . plugin . t ( "selectAll" ) } ) ;
2026-05-03 11:49:14 +00:00
allBtn . addClass ( "sfv-dd-action-btn" ) ;
2026-05-03 10:44:44 +00:00
allBtn . addEventListener ( "click" , ( e ) => {
2026-05-03 09:49:29 +00:00
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 ( ) ;
} ) ;
2026-05-03 10:44:44 +00:00
const clearBtn = actionBar . createEl ( "button" , { text : this . plugin . t ( "selectNone" ) } ) ;
2026-05-03 11:49:14 +00:00
clearBtn . addClass ( "sfv-dd-action-btn" ) ;
2026-05-03 10:44:44 +00:00
clearBtn . addEventListener ( "click" , ( e ) => {
2026-05-03 09:49:29 +00:00
e . preventDefault ( ) ;
e . stopPropagation ( ) ;
currentValues = [ ] ;
for ( const { row } of rows ) {
const cb = row . querySelector ( 'input[type="checkbox"]' ) ;
if ( cb ) cb . checked = false ;
}
onChange ( [ ] ) ;
refreshSummary ( ) ;
} ) ;
2026-05-03 10:44:44 +00:00
const search = panel . createEl ( "input" ) ;
search . type = "search" ;
search . placeholder = this . plugin . t ( "search" ) ;
2026-05-03 11:49:14 +00:00
search . addClass ( "sfv-dd-search" ) ;
2026-05-03 10:44:44 +00:00
const list = panel . createEl ( "div" ) ;
2026-05-03 09:49:29 +00:00
const rows = [ ] ;
for ( const option of options ) {
2026-05-03 10:44:44 +00:00
const row = list . createEl ( "label" ) ;
2026-05-03 11:49:14 +00:00
row . addClass ( "sfv-dd-row" ) ;
2026-05-03 10:44:44 +00:00
const cb = row . createEl ( "input" ) ;
cb . type = "checkbox" ;
2026-05-03 09:49:29 +00:00
cb . checked = currentValues . includes ( option ) ;
2026-05-03 10:44:44 +00:00
row . createEl ( "span" , { text : toDisplayText ( option ) } ) ;
cb . addEventListener ( "change" , ( ) => {
const next = cb . checked ? [ ... currentValues , option ] : currentValues . filter ( ( v ) => v !== option ) ;
2026-05-03 09:49:29 +00:00
currentValues = [ ... new Set ( next ) ] ;
onChange ( currentValues ) ;
refreshSummary ( ) ;
} ) ;
rows . push ( { option , row } ) ;
}
2026-05-03 10:44:44 +00:00
search . addEventListener ( "input" , ( ) => {
2026-05-03 09:49:29 +00:00
const q = search . value . toLowerCase ( ) ;
2026-05-03 11:49:14 +00:00
for ( const { option , row } of rows ) {
if ( ! q || option . toLowerCase ( ) . includes ( q ) ) row . removeClass ( "sfv-hidden" ) ;
else row . addClass ( "sfv-hidden" ) ;
}
2026-05-03 09:49:29 +00:00
} ) ;
2026-05-03 10:44:44 +00:00
box . addEventListener ( "toggle" , ( ) => {
2026-05-03 09:49:29 +00:00
if ( box . open ) {
2026-05-03 11:49:14 +00:00
wrap . addClass ( "sfv-elevated" ) ;
box . addClass ( "sfv-elevated" ) ;
2026-05-03 09:49:29 +00:00
} else {
2026-05-03 11:49:14 +00:00
wrap . removeClass ( "sfv-elevated" ) ;
box . removeClass ( "sfv-elevated" ) ;
2026-05-03 09:49:29 +00:00
if ( onCommit ) onCommit ( ) ;
}
} ) ;
}
2026-05-03 11:49:14 +00:00
renderView ( ) {
2026-05-03 09:49:29 +00:00
const profile = this . getRenderProfile ( ) ;
const container = this . containerEl . children [ 1 ] ;
container . empty ( ) ;
2026-05-03 10:44:44 +00:00
container . classList . add ( "smart-folder-view-root" ) ;
2026-05-03 09:49:29 +00:00
this . updateLeafTitle ( ) ;
if ( ! profile ) {
2026-05-03 10:44:44 +00:00
container . createEl ( "p" , { text : this . plugin . t ( "noProfile" ) } ) ;
2026-05-03 09:49:29 +00:00
return ;
}
this . ensureState ( profile . id , profile . sortOrder , profile . viewMode ) ;
2026-05-03 10:44:44 +00:00
const topBar = container . createEl ( "div" ) ;
2026-05-03 11:49:14 +00:00
topBar . addClass ( "sfv-top-bar" ) ;
2026-05-03 10:44:44 +00:00
const saveBtn = topBar . createEl ( "button" , { text : this . plugin . isDraft ( profile . id ) ? this . plugin . t ( "savePage" ) : this . plugin . t ( "updatePage" ) } ) ;
saveBtn . addClass ( "mod-cta" ) ;
2026-05-03 12:52:40 +00:00
saveBtn . addEventListener ( "click" , ( ) => {
void ( async ( ) => {
try {
await this . plugin . saveProfile ( profile , true , false ) ;
await this . rerenderPreserveScroll ( ) ;
} catch {
new obsidian . Notice ( this . plugin . t ( "saveFailed" ) ) ;
}
} ) ( ) ;
2026-05-03 09:49:29 +00:00
} ) ;
2026-05-03 10:44:44 +00:00
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 ( ) ) ;
2026-05-03 11:49:14 +00:00
topBar . createEl ( "button" , { text : this . plugin . t ( "exportPage" ) } ) . addEventListener ( "click" , ( ) => {
void this . plugin . exportProfileToMarkdown ( profile ) ;
2026-05-03 09:49:29 +00:00
} ) ;
2026-05-03 11:49:14 +00:00
topBar . createEl ( "button" , { text : this . plugin . t ( "refreshPreview" ) } ) . addEventListener ( "click" , ( ) => {
this . renderView ( ) ;
2026-05-03 10:44:44 +00:00
new obsidian . Notice ( this . plugin . t ( "refreshPreviewDone" ) ) ;
2026-05-03 09:49:29 +00:00
} ) ;
2026-06-27 20:00:30 +00:00
const files = this . plugin . getFilesByFolder ( profile . sourceFolder , profile . sourceNotePath ) ;
2026-05-03 09:49:29 +00:00
if ( ! files . length ) {
2026-05-03 10:44:44 +00:00
container . createEl ( "p" , { text : this . plugin . t ( "noFiles" , { folder : profile . sourceFolder } ) } ) ;
2026-05-03 09:49:29 +00:00
return ;
}
2026-06-04 10:46:05 +00:00
if ( this . filterState . boardField !== null && this . filterState . boardField !== void 0 ) {
profile . boardField = this . filterState . boardField ;
}
2026-05-03 10:44:44 +00:00
const entries = files . map ( ( file ) => ( { file , cache : this . app . metadataCache . getFileCache ( file ) } ) ) ;
2026-05-03 09:49:29 +00:00
for ( const entry of entries ) {
this . reconcilePendingBoardValue ( entry . file , entry . cache , profile . boardField ) ;
}
2026-05-03 10:44:44 +00:00
const attrKeys = getAllFrontmatterKeys ( entries . map ( ( e ) => e . cache ) ) ;
2026-05-03 09:49:29 +00:00
const optionsByAttr = { } ;
for ( const key of attrKeys ) {
2026-05-03 10:44:44 +00:00
const values = uniqueSorted ( entries . flatMap ( ( e ) => getFrontmatterValues ( e . cache , key ) ) ) ;
const hasEmpty = entries . some ( ( e ) => getFrontmatterValues ( e . cache , key ) . length === 0 ) ;
2026-05-03 09:49:29 +00:00
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 ] ) ) {
2026-05-03 10:44:44 +00:00
this . filterState . selectedByAttr [ key ] = [ ... optionsByAttr [ key ] || [ ] ] ;
2026-05-03 09:49:29 +00:00
defaultSelectionInitialized = true ;
}
}
if ( defaultSelectionInitialized ) this . saveState ( ) ;
2026-05-03 10:44:44 +00:00
const switchesEl = container . createEl ( "div" ) ;
2026-05-03 11:49:14 +00:00
switchesEl . addClass ( "sfv-switches" ) ;
2026-05-03 09:49:29 +00:00
for ( const key of attrKeys ) {
const enabled = this . filterState . enabledFilters . includes ( key ) ;
2026-05-03 10:44:44 +00:00
const chip = switchesEl . createEl ( "label" ) ;
2026-05-03 11:49:14 +00:00
chip . addClass ( "sfv-chip" ) ;
2026-05-03 10:44:44 +00:00
const cb = chip . createEl ( "input" ) ;
cb . type = "checkbox" ;
2026-05-03 09:49:29 +00:00
cb . checked = enabled ;
2026-05-03 10:44:44 +00:00
chip . createEl ( "span" , { text : key } ) ;
cb . addEventListener ( "change" , ( ) => {
2026-05-03 09:49:29 +00:00
const s = new Set ( this . filterState . enabledFilters ) ;
2026-05-03 10:44:44 +00:00
if ( cb . checked ) s . add ( key ) ;
else s . delete ( key ) ;
this . filterState . enabledFilters = attrKeys . filter ( ( k ) => s . has ( k ) ) ;
2026-05-03 09:49:29 +00:00
this . saveState ( ) ;
2026-05-03 11:49:14 +00:00
void this . renderView ( ) ;
2026-05-03 09:49:29 +00:00
} ) ;
}
2026-05-03 10:44:44 +00:00
const controls = container . createEl ( "div" ) ;
2026-05-03 11:49:14 +00:00
controls . addClass ( "sfv-controls" ) ;
controls . createEl ( "p" , { text : this . plugin . t ( "presetScopeDesc" ) } ) . addClass ( "sfv-controls-hint" ) ;
2026-05-03 09:49:29 +00:00
const presets = this . loadPresets ( ) ;
2026-05-03 10:44:44 +00:00
const presetWrap = controls . createEl ( "div" ) ;
2026-05-03 11:49:14 +00:00
presetWrap . addClass ( "sfv-preset-wrap" ) ;
presetWrap . createEl ( "span" , { text : this . plugin . t ( "preset" ) } ) . addClass ( "sfv-dd-label" ) ;
2026-05-03 10:44:44 +00:00
const presetSel = presetWrap . createEl ( "select" ) ;
presetSel . createEl ( "option" , { text : this . plugin . t ( "noPreset" ) , value : "" } ) ;
2026-05-03 09:49:29 +00:00
for ( const p of presets ) {
2026-05-03 10:44:44 +00:00
const opt = presetSel . createEl ( "option" , { text : p . name , value : p . name } ) ;
2026-05-03 09:49:29 +00:00
if ( p . name === this . filterState . presetName ) opt . selected = true ;
}
2026-05-03 10:44:44 +00:00
presetSel . addEventListener ( "change" , ( ) => {
2026-05-03 09:49:29 +00:00
const currentPresets = this . loadPresets ( ) ;
2026-05-03 10:44:44 +00:00
const p = currentPresets . find ( ( x ) => x . name === presetSel . value ) ;
2026-05-03 09:49:29 +00:00
if ( ! p ) {
2026-05-03 10:44:44 +00:00
this . filterState . presetName = "" ;
2026-05-03 09:49:29 +00:00
} 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 ;
2026-06-04 10:46:05 +00:00
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 ( ) ;
}
2026-05-03 09:49:29 +00:00
}
this . saveState ( ) ;
2026-05-03 11:49:14 +00:00
void this . renderView ( ) ;
2026-05-03 09:49:29 +00:00
} ) ;
2026-05-03 10:44:44 +00:00
makeSelectSearchable ( presetSel , this . plugin . t ( "search" ) ) ;
2026-05-03 11:49:14 +00:00
presetWrap . createEl ( "button" , { text : this . plugin . t ( "savePreset" ) } ) . addEventListener ( "click" , ( ) => {
void ( async ( ) => {
2026-06-04 10:46:05 +00:00
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" ) ) ;
2026-05-03 11:49:14 +00:00
}
} ) ( ) ;
2026-05-03 09:49:29 +00:00
} ) ;
2026-05-03 10:44:44 +00:00
presetWrap . createEl ( "button" , { text : this . plugin . t ( "delPreset" ) } ) . addEventListener ( "click" , ( ) => {
2026-05-03 09:49:29 +00:00
if ( ! this . filterState . presetName ) return ;
const currentPresets = this . loadPresets ( ) ;
2026-05-03 10:44:44 +00:00
this . savePresets ( currentPresets . filter ( ( p ) => p . name !== this . filterState . presetName ) ) ;
this . filterState . presetName = "" ;
2026-05-03 09:49:29 +00:00
this . saveState ( ) ;
2026-05-03 11:49:14 +00:00
void this . renderView ( ) ;
2026-05-03 09:49:29 +00:00
} ) ;
2026-05-03 10:44:44 +00:00
const filterGrid = controls . createEl ( "div" ) ;
2026-05-03 11:49:14 +00:00
filterGrid . addClass ( "sfv-filter-grid" ) ;
2026-05-03 09:49:29 +00:00
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 ;
2026-05-03 10:44:44 +00:00
const sortWrap = controls . createEl ( "label" ) ;
2026-05-03 11:49:14 +00:00
sortWrap . addClass ( "sfv-sort-wrap" ) ;
sortWrap . createEl ( "span" , { text : this . plugin . t ( "sortOrder" ) } ) . addClass ( "sfv-dd-label" ) ;
2026-05-03 10:44:44 +00:00
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" ) } ) ;
2026-05-03 09:49:29 +00:00
sortSel . value = this . filterState . sortOrder ;
2026-05-03 10:44:44 +00:00
makeSelectSearchable ( sortSel , this . plugin . t ( "search" ) ) ;
sortSel . dispatchEvent ( new Event ( "change" ) ) ;
sortSel . addEventListener ( "change" , ( ) => {
2026-05-03 09:49:29 +00:00
this . filterState . sortOrder = sortSel . value ;
this . saveState ( ) ;
2026-05-03 11:49:14 +00:00
void this . renderView ( ) ;
2026-05-03 09:49:29 +00:00
} ) ;
2026-05-03 10:44:44 +00:00
const modeWrap = controls . createEl ( "label" ) ;
2026-05-03 11:49:14 +00:00
modeWrap . addClass ( "sfv-sort-wrap" ) ;
modeWrap . createEl ( "span" , { text : this . plugin . t ( "viewMode" ) } ) . addClass ( "sfv-dd-label" ) ;
2026-05-03 10:44:44 +00:00
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" ) } ) ;
2026-05-03 09:49:29 +00:00
modeSel . value = this . filterState . viewMode ;
2026-05-03 10:44:44 +00:00
makeSelectSearchable ( modeSel , this . plugin . t ( "search" ) ) ;
modeSel . dispatchEvent ( new Event ( "change" ) ) ;
2026-05-03 11:49:14 +00:00
modeSel . addEventListener ( "change" , ( ) => {
2026-05-03 10:44:44 +00:00
this . filterState . viewMode = modeSel . value === "board" ? "board" : "timeline" ;
2026-05-03 09:49:29 +00:00
this . saveState ( ) ;
2026-05-03 11:49:14 +00:00
void this . renderView ( ) ;
2026-05-03 09:49:29 +00:00
} ) ;
2026-05-03 10:44:44 +00:00
if ( this . filterState . viewMode === "board" ) {
const boardFieldWrap = controls . createEl ( "label" ) ;
2026-05-03 11:49:14 +00:00
boardFieldWrap . addClass ( "sfv-sort-wrap" ) ;
boardFieldWrap . createEl ( "span" , { text : this . plugin . t ( "boardField" ) } ) . addClass ( "sfv-dd-label" ) ;
2026-05-03 10:44:44 +00:00
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 } ) ;
2026-06-04 10:46:05 +00:00
const effectiveBoardField = this . filterState . boardField !== null ? this . filterState . boardField : profile . boardField || "" ;
boardFieldSel . value = effectiveBoardField ;
2026-05-03 10:44:44 +00:00
makeSelectSearchable ( boardFieldSel , this . plugin . t ( "search" ) ) ;
2026-05-03 11:49:14 +00:00
boardFieldSel . addEventListener ( "change" , ( ) => {
2026-06-04 10:46:05 +00:00
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 ( ) ;
2026-05-03 09:49:29 +00:00
} ) ;
}
const filtered = entries . filter ( ( { file , cache } ) => {
if ( ! this . filterState . enabledFilters . length ) return true ;
for ( const key of this . filterState . enabledFilters ) {
2026-05-03 10:44:44 +00:00
const selected = Array . isArray ( this . filterState . selectedByAttr [ key ] ) ? this . filterState . selectedByAttr [ key ] : [ ... optionsByAttr [ key ] || [ ] ] ;
2026-05-03 09:49:29 +00:00
if ( ! selected . length ) return false ;
2026-05-03 10:44:44 +00:00
const vals = key === profile . boardField ? this . getBoardFieldValues ( file , cache , profile . boardField ) : getFrontmatterValues ( cache , key ) ;
2026-05-03 09:49:29 +00:00
const matchEmpty = selected . includes ( EMPTY _OPTION _VALUE ) && vals . length === 0 ;
2026-05-03 10:44:44 +00:00
const matchNormal = selected . some ( ( v ) => v !== EMPTY _OPTION _VALUE && vals . includes ( v ) ) ;
2026-05-03 09:49:29 +00:00
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 ;
2026-06-04 10:46:05 +00:00
const pa = parseSortValue ( va , profile . sortField ) ;
const pb = parseSortValue ( vb , profile . sortField ) ;
2026-05-03 10:44:44 +00:00
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 ;
2026-05-03 09:49:29 +00:00
} ) ;
2026-05-03 11:49:14 +00:00
container . createEl ( "p" , { text : this . plugin . t ( "matched" , { count : filtered . length } ) } ) . addClass ( "sfv-matched" ) ;
2026-05-03 09:49:29 +00:00
if ( ! filtered . length ) {
2026-05-03 10:44:44 +00:00
container . createEl ( "p" , { text : this . plugin . t ( "noMatch" ) } ) ;
2026-05-03 09:49:29 +00:00
return ;
}
2026-05-03 10:44:44 +00:00
if ( this . filterState . viewMode === "timeline" ) {
2026-05-03 09:49:29 +00:00
const ordered = this . applyOrder ( profile . id , this . orderKey ( profile ) , filtered ) ;
2026-05-03 10:44:44 +00:00
const orderActions2 = container . createEl ( "div" ) ;
2026-05-03 11:49:14 +00:00
orderActions2 . addClass ( "sfv-order-actions" ) ;
2026-05-03 10:44:44 +00:00
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" ) ) ;
2026-05-03 09:49:29 +00:00
} ) ;
2026-05-03 12:52:40 +00:00
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 ( ) ;
} ) ( ) ;
2026-05-03 09:49:29 +00:00
} ) ;
2026-05-03 12:52:40 +00:00
orderActions2 . createEl ( "button" , { text : this . plugin . t ( "undoLastAction" ) } ) . addEventListener ( "click" , ( ) => {
void this . undoLastAction ( profile ) ;
2026-05-03 09:49:29 +00:00
} ) ;
2026-05-03 10:44:44 +00:00
const timelineEl = container . createEl ( "div" ) ;
2026-05-03 11:49:14 +00:00
timelineEl . addClass ( "sfv-timeline" ) ;
2026-05-03 10:44:44 +00:00
const lineEl = timelineEl . createEl ( "div" ) ;
2026-05-03 11:49:14 +00:00
lineEl . addClass ( "sfv-timeline-line" ) ;
2026-05-03 10:44:44 +00:00
const indicator = timelineEl . createEl ( "div" ) ;
2026-05-03 11:49:14 +00:00
indicator . addClass ( "sfv-timeline-indicator" ) ;
2026-05-03 09:49:29 +00:00
let dropBeforePath = null ;
const hideIndicator = ( ) => {
2026-05-03 11:49:14 +00:00
indicator . removeClass ( "sfv-visible" ) ;
2026-05-03 09:49:29 +00:00
} ;
const updateDropIndicator = ( clientY ) => {
2026-05-03 10:44:44 +00:00
const cards = Array . from ( timelineEl . querySelectorAll ( "section[data-sfv-path]" ) ) . filter ( ( el ) => ! el . classList . contains ( "sfv-dragging" ) ) ;
2026-05-03 09:49:29 +00:00
if ( ! cards . length ) {
dropBeforePath = null ;
2026-05-03 11:54:56 +00:00
indicator . setCssProps ( { "--sfv-top" : "0px" } ) ;
2026-05-03 11:49:14 +00:00
indicator . addClass ( "sfv-visible" ) ;
2026-05-03 09:49:29 +00:00
return ;
}
let beforeCard = null ;
for ( const cardEl of cards ) {
const rect = cardEl . getBoundingClientRect ( ) ;
if ( clientY < rect . top + rect . height / 2 ) {
beforeCard = cardEl ;
break ;
}
}
2026-05-03 10:44:44 +00:00
dropBeforePath = beforeCard ? . getAttribute ( "data-sfv-path" ) || null ;
2026-05-03 09:49:29 +00:00
let top = 0 ;
if ( beforeCard ) {
top = beforeCard . offsetTop - 2 ;
} else {
const last = cards [ cards . length - 1 ] ;
top = last . offsetTop + last . offsetHeight - 2 ;
}
2026-05-03 11:54:56 +00:00
indicator . setCssProps ( { "--sfv-top" : ` ${ Math . max ( 0 , top ) } px ` } ) ;
2026-05-03 11:49:14 +00:00
indicator . addClass ( "sfv-visible" ) ;
2026-05-03 09:49:29 +00:00
} ;
2026-05-03 10:44:44 +00:00
timelineEl . addEventListener ( "dragover" , ( e ) => {
2026-05-03 09:49:29 +00:00
e . preventDefault ( ) ;
2026-05-03 10:44:44 +00:00
if ( e . dataTransfer ) e . dataTransfer . dropEffect = "move" ;
2026-05-03 09:49:29 +00:00
updateDropIndicator ( e . clientY ) ;
} ) ;
2026-05-03 10:44:44 +00:00
timelineEl . addEventListener ( "dragleave" , ( e ) => {
2026-05-03 09:49:29 +00:00
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 ;
2026-05-03 10:44:44 +00:00
const from = ordered . findIndex ( ( e ) => e . file . path === path ) ;
2026-05-03 09:49:29 +00:00
if ( from < 0 ) return ;
const moved = ordered [ from ] ;
2026-05-03 10:44:44 +00:00
const nextOrdered = ordered . filter ( ( e ) => e . file . path !== path ) ;
const to = beforePath ? nextOrdered . findIndex ( ( e ) => e . file . path === beforePath ) : - 1 ;
2026-05-03 09:49:29 +00:00
const insertAt = to >= 0 ? to : nextOrdered . length ;
nextOrdered . splice ( insertAt , 0 , moved ) ;
2026-05-03 10:44:44 +00:00
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" )
2026-05-03 09:49:29 +00:00
} ) ;
2026-05-03 10:44:44 +00:00
if ( action === "cancel" ) {
new obsidian . Notice ( this . plugin . t ( "timelineMoveNoChange" ) ) ;
2026-05-03 09:49:29 +00:00
return ;
}
this . timelineDropBusy = true ;
try {
this . recordUndoSnapshot ( profile ) ;
ordered . splice ( 0 , ordered . length , ... nextOrdered ) ;
2026-05-03 10:44:44 +00:00
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 } ) ) ;
2026-05-03 09:49:29 +00:00
if ( next != null ) {
const value = next . trim ( ) ;
2026-05-03 10:44:44 +00:00
await this . app . fileManager . processFrontMatter ( moved . file , ( fm ) => {
2026-05-03 09:49:29 +00:00
if ( value ) fm [ field ] = value ;
else delete fm [ field ] ;
} ) ;
}
2026-05-03 10:44:44 +00:00
new obsidian . Notice ( this . plugin . t ( "timelineMoveDone" ) ) ;
2026-05-03 09:49:29 +00:00
await this . rerenderPreserveScroll ( ) ;
} else {
2026-05-03 10:44:44 +00:00
const cardEls = /* @__PURE__ */ new Map ( ) ;
for ( const el of timelineEl . querySelectorAll ( "section[data-sfv-path]" ) ) {
cardEls . set ( el . getAttribute ( "data-sfv-path" ) , el ) ;
2026-05-03 09:49:29 +00:00
}
for ( const entry of ordered ) {
const el = cardEls . get ( entry . file . path ) ;
2026-05-03 10:44:44 +00:00
if ( el ) timelineEl . appendChild ( el ) ;
2026-05-03 09:49:29 +00:00
}
2026-05-03 10:44:44 +00:00
new obsidian . Notice ( this . plugin . t ( "timelineMoveDone" ) ) ;
2026-05-03 09:49:29 +00:00
}
} finally {
this . timelineDropBusy = false ;
}
} ;
2026-05-03 11:49:14 +00:00
timelineEl . addEventListener ( "drop" , ( e ) => {
2026-05-03 09:49:29 +00:00
e . preventDefault ( ) ;
hideIndicator ( ) ;
2026-05-03 10:44:44 +00:00
const payload = e . dataTransfer ? . getData ( "text/plain" ) || "" ;
2026-05-03 09:49:29 +00:00
if ( ! payload ) return ;
const obj = JSON . parse ( payload ) ;
if ( obj . token && this . timelineDragToken && obj . token !== this . timelineDragToken ) return ;
2026-05-03 10:44:44 +00:00
if ( ! obj . path || obj . from !== "timeline" ) return ;
2026-05-03 09:49:29 +00:00
const beforePath = dropBeforePath ;
2026-05-03 11:49:14 +00:00
void handleTimelineDrop ( obj . path , beforePath ) ;
2026-05-03 09:49:29 +00:00
} ) ;
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 ;
2026-05-03 10:44:44 +00:00
card . setAttribute ( "data-sfv-path" , file . path ) ;
2026-05-03 09:49:29 +00:00
card . draggable = true ;
2026-05-03 10:44:44 +00:00
card . addEventListener ( "dragstart" , ( e ) => {
2026-05-03 09:49:29 +00:00
this . timelineDragToken = newId ( ) ;
2026-05-03 10:44:44 +00:00
card . classList . add ( "sfv-dragging" ) ;
2026-05-03 09:49:29 +00:00
if ( e . dataTransfer ) {
2026-05-03 10:44:44 +00:00
e . dataTransfer . effectAllowed = "move" ;
2026-05-03 09:49:29 +00:00
e . dataTransfer . setDragImage ( card , 28 , 20 ) ;
}
2026-05-03 10:44:44 +00:00
e . dataTransfer ? . setData ( "text/plain" , JSON . stringify ( { path : file . path , from : "timeline" , token : this . timelineDragToken } ) ) ;
2026-05-03 09:49:29 +00:00
} ) ;
2026-05-03 10:44:44 +00:00
card . addEventListener ( "dragend" , ( ) => {
card . classList . remove ( "sfv-dragging" ) ;
this . timelineDragToken = "" ;
2026-05-03 09:49:29 +00:00
hideIndicator ( ) ;
} ) ;
2026-05-03 10:44:44 +00:00
card . addEventListener ( "dragover" , ( e ) => e . preventDefault ( ) ) ;
2026-05-03 09:49:29 +00:00
}
return ;
}
2026-05-03 10:44:44 +00:00
const grouped = /* @__PURE__ */ new Map ( ) ;
const unsetLabel = this . plugin . t ( "boardUnset" ) ;
2026-05-03 09:49:29 +00:00
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 ( ) ] ) ;
2026-05-03 10:44:44 +00:00
const orderActions = container . createEl ( "div" ) ;
2026-05-03 11:49:14 +00:00
orderActions . addClass ( "sfv-order-actions" ) ;
2026-05-03 10:44:44 +00:00
orderActions . createEl ( "button" , { text : this . plugin . t ( "saveOrder" ) } ) . addEventListener ( "click" , ( ) => {
2026-05-03 09:49:29 +00:00
for ( const col of columns ) {
const arr = grouped . get ( col ) || [ ] ;
2026-05-03 10:44:44 +00:00
this . saveOrder ( profile . id , this . orderKey ( profile , col ) , arr . map ( ( e ) => e . file ) ) ;
2026-05-03 09:49:29 +00:00
}
2026-05-03 10:44:44 +00:00
new obsidian . Notice ( this . plugin . t ( "saveOrderDone" ) ) ;
2026-05-03 09:49:29 +00:00
} ) ;
2026-05-03 12:52:40 +00:00
orderActions . createEl ( "button" , { text : this . plugin . t ( "resetOrder" ) } ) . addEventListener ( "click" , ( ) => {
2026-05-03 09:49:29 +00:00
this . recordUndoSnapshot ( profile ) ;
for ( const col of columns ) this . clearOrder ( profile . id , this . orderKey ( profile , col ) ) ;
2026-05-03 10:44:44 +00:00
new obsidian . Notice ( this . plugin . t ( "resetOrderDone" ) ) ;
2026-05-03 12:52:40 +00:00
this . renderView ( ) ;
2026-05-03 09:49:29 +00:00
} ) ;
2026-05-03 12:52:40 +00:00
orderActions . createEl ( "button" , { text : this . plugin . t ( "undoLastAction" ) } ) . addEventListener ( "click" , ( ) => {
void this . undoLastAction ( profile ) ;
2026-05-03 09:49:29 +00:00
} ) ;
2026-05-03 10:44:44 +00:00
const boardEl = container . createEl ( "div" ) ;
2026-05-03 11:49:14 +00:00
boardEl . addClass ( "sfv-board" ) ;
2026-05-03 09:49:29 +00:00
const handleDrop = async ( payload , to , beforePath ) => {
const fromArr = grouped . get ( payload . from ) || [ ] ;
const toArr = grouped . get ( to ) || [ ] ;
2026-05-03 10:44:44 +00:00
const idx = fromArr . findIndex ( ( e ) => e . file . path === payload . path ) ;
2026-05-03 09:49:29 +00:00
if ( idx < 0 ) return ;
2026-05-03 10:44:44 +00:00
let action = "order" ;
let nextBoardValue = void 0 ;
2026-05-03 09:49:29 +00:00
if ( payload . from !== to && profile . boardField ) {
2026-05-03 10:44:44 +00:00
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" )
2026-05-03 09:49:29 +00:00
} ) ;
2026-05-03 10:44:44 +00:00
if ( action === "cancel" ) {
new obsidian . Notice ( this . plugin . t ( "boardMoveNoChange" ) ) ;
2026-05-03 09:49:29 +00:00
return ;
}
2026-05-03 10:44:44 +00:00
nextBoardValue = to === unsetLabel ? "" : to ;
2026-05-03 09:49:29 +00:00
}
this . recordUndoSnapshot ( profile ) ;
const removed = fromArr . splice ( idx , 1 ) [ 0 ] ;
2026-05-03 10:44:44 +00:00
const pos = beforePath ? toArr . findIndex ( ( e ) => e . file . path === beforePath ) : - 1 ;
2026-05-03 09:49:29 +00:00
const insertAt = pos >= 0 ? pos : toArr . length ;
toArr . splice ( insertAt , 0 , removed ) ;
grouped . set ( payload . from , fromArr ) ;
grouped . set ( to , toArr ) ;
2026-05-03 10:44:44 +00:00
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" ) {
2026-05-03 09:49:29 +00:00
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 ] ;
}
2026-05-03 10:44:44 +00:00
await this . app . fileManager . processFrontMatter ( removed . file , ( fm ) => {
2026-05-03 09:49:29 +00:00
if ( nextBoardValue ) fm [ profile . boardField ] = nextBoardValue ;
else delete fm [ profile . boardField ] ;
} ) ;
}
2026-05-03 10:44:44 +00:00
new obsidian . Notice ( this . plugin . t ( "boardMoveDone" ) ) ;
2026-05-03 09:49:29 +00:00
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 ) ;
2026-05-03 10:44:44 +00:00
const colWrap = boardEl . createEl ( "section" ) ;
2026-05-03 11:49:14 +00:00
colWrap . addClass ( "sfv-board-col" ) ;
2026-05-04 00:29:43 +00:00
colWrap . createEl ( "strong" , { text : ` ${ col } ( ${ ordered . length } ) ` } ) ;
2026-05-03 10:44:44 +00:00
const cardsEl = colWrap . createEl ( "div" ) ;
2026-05-03 11:49:14 +00:00
cardsEl . addClass ( "sfv-board-cards" ) ;
2026-05-03 10:44:44 +00:00
const dropIndicator = cardsEl . createEl ( "div" ) ;
2026-05-03 11:49:14 +00:00
dropIndicator . addClass ( "sfv-drop-indicator" ) ;
2026-05-03 09:49:29 +00:00
let dropBeforePath = null ;
const hideDropIndicator = ( ) => {
2026-05-03 11:49:14 +00:00
dropIndicator . removeClass ( "sfv-visible" ) ;
2026-05-03 09:49:29 +00:00
dropBeforePath = null ;
} ;
const getNextCardPath = ( cardEl ) => {
let next = cardEl . nextElementSibling ;
while ( next ) {
2026-05-03 10:44:44 +00:00
if ( next . matches ? . ( "section[data-sfv-path]" ) ) return next . getAttribute ( "data-sfv-path" ) || null ;
2026-05-03 09:49:29 +00:00
next = next . nextElementSibling ;
}
return null ;
} ;
const getCardDropBeforePath = ( cardEl , clientY ) => {
const rect = cardEl . getBoundingClientRect ( ) ;
2026-05-03 10:44:44 +00:00
if ( clientY < rect . top + rect . height / 2 ) return cardEl . getAttribute ( "data-sfv-path" ) || null ;
2026-05-03 09:49:29 +00:00
return getNextCardPath ( cardEl ) ;
} ;
const updateDropIndicator = ( clientY ) => {
2026-05-03 10:44:44 +00:00
const cards = Array . from ( cardsEl . querySelectorAll ( "section[data-sfv-path]" ) ) . filter ( ( el ) => ! el . classList . contains ( "sfv-dragging" ) ) ;
2026-05-03 09:49:29 +00:00
if ( ! cards . length ) {
dropBeforePath = null ;
cardsEl . appendChild ( dropIndicator ) ;
2026-05-03 11:49:14 +00:00
dropIndicator . addClass ( "sfv-visible" ) ;
2026-05-03 09:49:29 +00:00
return ;
}
let beforeCard = null ;
for ( const cardEl of cards ) {
const rect = cardEl . getBoundingClientRect ( ) ;
if ( clientY < rect . top + rect . height / 2 ) {
beforeCard = cardEl ;
break ;
}
}
2026-05-03 10:44:44 +00:00
dropBeforePath = beforeCard ? . getAttribute ( "data-sfv-path" ) || null ;
2026-05-03 09:49:29 +00:00
if ( beforeCard ) cardsEl . insertBefore ( dropIndicator , beforeCard ) ;
else cardsEl . appendChild ( dropIndicator ) ;
2026-05-03 11:49:14 +00:00
dropIndicator . addClass ( "sfv-visible" ) ;
2026-05-03 09:49:29 +00:00
} ;
2026-05-03 10:44:44 +00:00
cardsEl . addEventListener ( "dragover" , ( e ) => {
2026-05-03 09:49:29 +00:00
e . preventDefault ( ) ;
2026-05-03 10:44:44 +00:00
if ( e . dataTransfer ) e . dataTransfer . dropEffect = "move" ;
2026-05-03 09:49:29 +00:00
updateDropIndicator ( e . clientY ) ;
} ) ;
2026-05-03 10:44:44 +00:00
cardsEl . addEventListener ( "dragleave" , ( e ) => {
2026-05-03 09:49:29 +00:00
const related = e . relatedTarget ;
if ( related instanceof Node && cardsEl . contains ( related ) ) return ;
hideDropIndicator ( ) ;
} ) ;
2026-05-03 11:49:14 +00:00
cardsEl . addEventListener ( "drop" , ( e ) => {
2026-05-03 09:49:29 +00:00
e . preventDefault ( ) ;
const beforePath = dropBeforePath ;
hideDropIndicator ( ) ;
2026-05-03 10:44:44 +00:00
const payload = e . dataTransfer ? . getData ( "text/plain" ) || "" ;
2026-05-03 09:49:29 +00:00
if ( ! payload ) return ;
2026-05-03 11:49:14 +00:00
void handleDrop ( JSON . parse ( payload ) , col , beforePath ) ;
2026-05-03 09:49:29 +00:00
} ) ;
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 ;
2026-05-03 10:44:44 +00:00
card . setAttribute ( "data-sfv-path" , file . path ) ;
2026-05-03 09:49:29 +00:00
card . draggable = true ;
2026-05-03 10:44:44 +00:00
card . addEventListener ( "dragstart" , ( e ) => {
card . classList . add ( "sfv-dragging" ) ;
e . dataTransfer ? . setData ( "text/plain" , JSON . stringify ( { path : file . path , from : col } ) ) ;
2026-05-03 09:49:29 +00:00
} ) ;
2026-05-03 10:44:44 +00:00
card . addEventListener ( "dragend" , ( ) => {
card . classList . remove ( "sfv-dragging" ) ;
2026-05-03 09:49:29 +00:00
hideDropIndicator ( ) ;
} ) ;
2026-05-03 10:44:44 +00:00
card . addEventListener ( "dragover" , ( e ) => {
2026-05-03 09:49:29 +00:00
e . preventDefault ( ) ;
updateDropIndicator ( e . clientY ) ;
} ) ;
2026-05-03 11:49:14 +00:00
card . addEventListener ( "drop" , ( e ) => {
2026-05-03 09:49:29 +00:00
e . preventDefault ( ) ;
e . stopPropagation ( ) ;
const beforePath = getCardDropBeforePath ( card , e . clientY ) ;
hideDropIndicator ( ) ;
2026-05-03 10:44:44 +00:00
const payload = e . dataTransfer ? . getData ( "text/plain" ) || "" ;
2026-05-03 09:49:29 +00:00
if ( ! payload ) return ;
2026-05-03 11:49:14 +00:00
void handleDrop ( JSON . parse ( payload ) , col , beforePath ) ;
2026-05-03 09:49:29 +00:00
} ) ;
}
}
}
2026-05-03 11:49:14 +00:00
onClose ( ) {
2026-05-03 10:44:44 +00:00
}
} ;
var SmartFolderSettingTab = class extends obsidian . PluginSettingTab {
2026-05-03 09:49:29 +00:00
constructor ( app , plugin ) {
super ( app , plugin ) ;
this . plugin = plugin ;
}
display ( ) {
const { containerEl } = this ;
containerEl . empty ( ) ;
2026-05-03 10:44:44 +00:00
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 ( ) ;
2026-05-03 09:49:29 +00:00
} ) ;
2026-05-03 10:44:44 +00:00
} ) ;
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 ( ) ;
} ) ) ;
2026-05-04 08:50:55 +00:00
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 ) => {
2026-05-03 10:44:44 +00:00
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 ( ) ;
} ) ) ;
2026-05-03 09:49:29 +00:00
}
2026-05-03 10:44:44 +00:00
} ;
var SmartFolderPlugin = class extends obsidian . Plugin {
2026-05-03 09:49:29 +00:00
t ( key , vars ) {
2026-05-03 10:44:44 +00:00
const mode = this . data ? . languageMode || "auto" ;
const lang = mode === "auto" ? detectLanguage ( ) : mode ;
2026-05-03 09:49:29 +00:00
return tpl ( I18N [ lang ] ? . [ key ] || key , vars ) ;
}
getFolderOptions ( ) {
2026-05-03 10:44:44 +00:00
const set = /* @__PURE__ */ new Set ( ) ;
2026-05-03 09:49:29 +00:00
for ( const f of this . app . vault . getMarkdownFiles ( ) ) {
if ( f . parent ? . path ) set . add ( f . parent . path ) ;
}
2026-05-03 10:44:44 +00:00
return [ "" , ... uniqueSorted ( [ ... set ] ) ] ;
2026-05-03 09:49:29 +00:00
}
2026-06-27 20:00:30 +00:00
getFilesByFolder ( sourceFolder , excludedPath = "" ) {
2026-06-04 10:46:05 +00:00
const files = this . app . vault . getMarkdownFiles ( ) . filter ( ( f ) => {
const cache = this . app . metadataCache . getFileCache ( f ) ;
2026-06-27 20:00:30 +00:00
return f . path !== excludedPath && ! isLikelySmartFolderExportPath ( f . path ) && ! isSmartFolderExportCache ( cache ) ;
2026-06-04 10:46:05 +00:00
} ) ;
if ( ! sourceFolder ) return files ;
2026-05-03 10:44:44 +00:00
const prefix = sourceFolder . replace ( /\/$/ , "" ) + "/" ;
2026-06-04 10:46:05 +00:00
return files . filter ( ( f ) => f . path . startsWith ( prefix ) || f . parent ? . path === sourceFolder ) ;
2026-05-03 09:49:29 +00:00
}
getTemplateFileOptions ( ) {
2026-05-03 10:44:44 +00:00
return this . app . vault . getMarkdownFiles ( ) . map ( ( f ) => f . path ) ;
2026-05-03 09:49:29 +00:00
}
2026-06-27 20:00:30 +00:00
getFieldCandidates ( sourceFolder , templateFile , excludedPath = "" ) {
2026-05-03 09:49:29 +00:00
const template = templateFile ? this . app . vault . getAbstractFileByPath ( templateFile ) : null ;
2026-05-03 10:44:44 +00:00
const templateKeys = template instanceof obsidian . TFile ? getAllFrontmatterKeys ( [ this . app . metadataCache . getFileCache ( template ) ] ) : [ ] ;
2026-06-27 20:00:30 +00:00
const files = this . getFilesByFolder ( sourceFolder , excludedPath ) ;
2026-05-03 10:44:44 +00:00
const caches = files . map ( ( f ) => this . app . metadataCache . getFileCache ( f ) ) ;
2026-05-03 09:49:29 +00:00
const allKeys = getAllFrontmatterKeys ( caches ) ;
2026-05-03 10:44:44 +00:00
const common = allKeys . filter ( ( k ) => caches . every ( ( c ) => ! c ? . frontmatter || k in c . frontmatter ) ) ;
2026-05-03 09:49:29 +00:00
return uniqueSorted ( [ ... templateKeys , ... common ] ) ;
}
getCurrentProfile ( ) {
if ( this . draftProfile ) return this . draftProfile ;
const id = this . data . lastActiveProfileId ;
2026-05-03 10:44:44 +00:00
return this . data . profiles . find ( ( p ) => p . id === id ) || null ;
2026-05-03 09:49:29 +00:00
}
isDraft ( id ) {
return ! ! this . draftProfile && this . draftProfile . id === id ;
}
setDraftProfile ( profile ) {
2026-05-03 10:44:44 +00:00
this . draftProfile = { ... profile , displayFields : [ ... profile . displayFields || [ ] ] , updatedAt : Date . now ( ) } ;
2026-05-03 09:49:29 +00:00
}
clearDraftProfile ( ) {
this . draftProfile = null ;
}
async saveProfile ( profile , setActive , askNameIfEmpty ) {
2026-05-03 10:44:44 +00:00
const next = { ... profile , displayFields : [ ... profile . displayFields || [ ] ] , updatedAt : Date . now ( ) } ;
2026-05-03 11:49:14 +00:00
if ( ! next . name && askNameIfEmpty ) next . name = ( await askTextInput ( this . app , this . t ( "pageNamePrompt" ) , this . t ( "pageNamePrompt" ) , next . sourceFolder || "" , "" ) || "" ) . trim ( ) ;
2026-05-03 10:44:44 +00:00
if ( ! next . name ) next . name = next . sourceFolder || ` Page- ${ ( /* @__PURE__ */ new Date ( ) ) . toISOString ( ) } ` ;
const idx = this . data . profiles . findIndex ( ( p ) => p . id === next . id ) ;
2026-05-03 09:49:29 +00:00
if ( idx >= 0 ) {
this . data . profiles [ idx ] = { ... this . data . profiles [ idx ] , ... next } ;
2026-05-03 10:44:44 +00:00
new obsidian . Notice ( this . t ( "pageUpdated" ) ) ;
2026-05-03 09:49:29 +00:00
} else {
next . createdAt = next . createdAt || Date . now ( ) ;
this . data . profiles . push ( next ) ;
2026-05-03 10:44:44 +00:00
new obsidian . Notice ( this . t ( "pageSaved" ) ) ;
2026-05-03 09:49:29 +00:00
}
if ( setActive ) this . data . lastActiveProfileId = next . id ;
if ( this . draftProfile ? . id === next . id ) this . draftProfile = null ;
await this . persist ( ) ;
}
normalizeExportProfile ( raw ) {
2026-05-03 10:44:44 +00:00
if ( ! raw || typeof raw !== "object" ) return null ;
2026-05-03 09:49:29 +00:00
const profile = {
id : String ( raw . id || newId ( ) ) ,
2026-05-03 10:44:44 +00:00
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 || "" ) ,
2026-06-27 20:00:30 +00:00
sourceNotePath : String ( raw . sourceNotePath || "" ) ,
2026-05-03 10:44:44 +00:00
createdAt : typeof raw . createdAt === "number" ? raw . createdAt : Date . now ( ) ,
updatedAt : Date . now ( )
2026-05-03 09:49:29 +00:00
} ;
return profile ;
}
2026-06-04 10:46:05 +00:00
async openProfileFromExport ( raw , opts = { } ) {
2026-05-03 09:49:29 +00:00
const profile = this . normalizeExportProfile ( raw ) ;
if ( ! profile ) return false ;
2026-06-27 20:00:30 +00:00
profile . sourceNotePath = String ( opts . sourcePath || profile . sourceNotePath || "" ) ;
2026-06-04 10:46:05 +00:00
await this . activateView ( {
profile ,
newLeaf : opts . newLeaf === true ,
persistLastActive : false ,
leaf : opts . leaf || null
} ) ;
2026-05-03 09:49:29 +00:00
return true ;
}
2026-06-27 20:00:30 +00:00
async importProfileFromExport ( raw , opts = { } ) {
2026-05-03 09:49:29 +00:00
const profile = this . normalizeExportProfile ( raw ) ;
if ( ! profile ) return false ;
2026-06-27 20:00:30 +00:00
profile . sourceNotePath = String ( opts . sourcePath || profile . sourceNotePath || "" ) ;
2026-05-03 10:44:44 +00:00
const existing = this . data . profiles . find ( ( p ) => p . id === profile . id ) || this . data . profiles . find ( ( p ) => p . name === profile . name && p . sourceFolder === profile . sourceFolder ) ;
2026-05-03 09:49:29 +00:00
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 ) {
2026-05-03 10:44:44 +00:00
new obsidian . Notice ( this . t ( "noProfile" ) ) ;
2026-05-03 09:49:29 +00:00
return ;
}
try {
const folders = this . getFolderOptions ( ) ;
2026-05-03 10:44:44 +00:00
const defaultFolder = this . app . vault . getAbstractFileByPath ( "00 Homepage" ) instanceof obsidian . TFolder ? "00 Homepage" : "" ;
2026-05-03 09:49:29 +00:00
const chosenFolder = await askFolderSelect ( this . app , this , folders , defaultFolder ) ;
if ( chosenFolder == null ) return ;
2026-05-03 10:44:44 +00:00
const safeBase = ( profile . name || profile . sourceFolder || "smart-folder-page" ) . replace ( /[\\/:*?"<>|]/g , "-" ) . trim ( ) || "smart-folder-page" ;
2026-05-04 00:29:43 +00:00
const folderPrefix = chosenFolder ? ` ${ chosenFolder } / ` : "" ;
2026-05-03 09:49:29 +00:00
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 ,
2026-05-03 10:44:44 +00:00
exportedAt : ( /* @__PURE__ */ new Date ( ) ) . toISOString ( )
2026-05-03 09:49:29 +00:00
} ;
const content = [
2026-05-03 10:44:44 +00:00
"# Smart Folder \u9875\u9762\u5BFC\u51FA" ,
"" ,
2026-06-04 10:46:05 +00:00
"<!-- smart-folder-view-page-export -->" ,
"" ,
2026-05-03 10:44:44 +00:00
"```smart-folder-page" ,
2026-05-03 09:49:29 +00:00
JSON . stringify ( payload , null , 2 ) ,
2026-05-03 10:44:44 +00:00
"```" ,
""
] . join ( "\n" ) ;
2026-05-03 09:49:29 +00:00
await this . app . vault . create ( path , content ) ;
2026-05-03 10:44:44 +00:00
new obsidian . Notice ( this . t ( "exportPageDone" , { path } ) ) ;
2026-05-03 09:49:29 +00:00
} catch {
2026-05-03 10:44:44 +00:00
new obsidian . Notice ( this . t ( "exportPageFailed" ) ) ;
2026-05-03 09:49:29 +00:00
}
}
async onload ( ) {
await this . loadPluginData ( ) ;
2026-05-03 10:44:44 +00:00
this . registerView ( VIEW _TYPE , ( leaf ) => new SmartFolderView ( leaf , this ) ) ;
2026-06-27 20:00:30 +00:00
this . registerMarkdownCodeBlockProcessor ( "smart-folder-page" , ( source , el , ctx ) => {
2026-05-03 09:49:29 +00:00
el . empty ( ) ;
let raw ;
try {
2026-05-03 10:44:44 +00:00
raw = JSON . parse ( source || "{}" ) ;
2026-05-03 09:49:29 +00:00
} catch {
2026-05-03 10:44:44 +00:00
el . createEl ( "p" , { text : this . t ( "exportBlockInvalid" ) } ) ;
2026-05-03 09:49:29 +00:00
return ;
}
const profile = this . normalizeExportProfile ( raw ) ;
if ( ! profile ) {
2026-05-03 10:44:44 +00:00
el . createEl ( "p" , { text : this . t ( "exportBlockInvalid" ) } ) ;
2026-05-03 09:49:29 +00:00
return ;
}
2026-05-03 10:44:44 +00:00
const wrap = el . createEl ( "div" ) ;
2026-05-03 11:49:14 +00:00
wrap . addClass ( "sfv-export-block" ) ;
2026-05-03 10:44:44 +00:00
wrap . createEl ( "strong" , { text : this . t ( "exportBlockTitle" ) } ) ;
2026-05-03 11:49:14 +00:00
wrap . createEl ( "p" , { text : ` ${ this . t ( "exportBlockDesc" ) } ${ profile . name || profile . sourceFolder } ` } ) . addClass ( "sfv-export-desc" ) ;
2026-05-03 10:44:44 +00:00
const row = wrap . createEl ( "div" ) ;
2026-05-03 11:49:14 +00:00
row . addClass ( "sfv-export-row" ) ;
2026-05-03 10:44:44 +00:00
const openBtn = row . createEl ( "button" , { text : this . t ( "exportBlockOpen" ) } ) ;
openBtn . addClass ( "mod-cta" ) ;
2026-05-03 11:49:14 +00:00
openBtn . addEventListener ( "click" , ( ) => {
2026-06-04 10:46:05 +00:00
const currentLeaf = this . app . workspace . getMostRecentLeaf ( ) ;
2026-06-27 20:00:30 +00:00
void this . openProfileFromExport ( raw , { leaf : currentLeaf , sourcePath : ctx . sourcePath } ) . then ( ( ok ) => {
2026-05-03 11:49:14 +00:00
if ( ! ok ) new obsidian . Notice ( this . t ( "exportBlockInvalid" ) ) ;
} ) ;
2026-05-03 09:49:29 +00:00
} ) ;
2026-05-03 10:44:44 +00:00
const importBtn = row . createEl ( "button" , { text : this . t ( "exportBlockImport" ) } ) ;
2026-05-03 11:49:14 +00:00
importBtn . addEventListener ( "click" , ( ) => {
2026-06-27 20:00:30 +00:00
void this . importProfileFromExport ( raw , { sourcePath : ctx . sourcePath } ) . then ( ( ok ) => {
2026-05-03 11:49:14 +00:00
if ( ! ok ) new obsidian . Notice ( this . t ( "exportBlockInvalid" ) ) ;
} ) ;
2026-05-03 09:49:29 +00:00
} ) ;
} ) ;
2026-05-03 10:44:44 +00:00
this . registerDomEvent ( document , "pointerdown" , ( ev ) => {
2026-05-03 09:49:29 +00:00
const target = ev . target ;
if ( ! ( target instanceof Element ) ) return ;
2026-05-03 10:44:44 +00:00
const openBoxes = document . querySelectorAll ( ".smart-folder-view-root details[open]" ) ;
2026-05-03 09:49:29 +00:00
for ( const box of openBoxes ) {
if ( box . contains ( target ) ) continue ;
2026-05-03 10:44:44 +00:00
box . removeAttribute ( "open" ) ;
2026-05-03 09:49:29 +00:00
}
} , true ) ;
2026-05-03 10:44:44 +00:00
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" ) ;
2026-05-03 09:49:29 +00:00
} ) ;
2026-05-03 11:49:14 +00:00
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 ( ) ;
} ) ;
2026-05-03 09:49:29 +00:00
} ) ;
2026-05-03 11:49:14 +00:00
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 } ) ;
} ) ( ) ;
2026-05-03 09:49:29 +00:00
} } ) ;
2026-05-03 11:49:14 +00:00
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 ) ;
} ) ( ) ;
2026-05-03 09:49:29 +00:00
} } ) ;
this . addSettingTab ( new SmartFolderSettingTab ( this . app , this ) ) ;
}
async activateView ( opts = { } ) {
2026-06-04 10:46:05 +00:00
const { profile = null , newLeaf = false , persistLastActive = true , leaf : preferredLeaf = null } = opts ;
2026-05-03 09:49:29 +00:00
const { workspace } = this . app ;
2026-06-04 10:46:05 +00:00
let leaf = preferredLeaf ;
if ( ! leaf && ! newLeaf ) leaf = workspace . getLeavesOfType ( VIEW _TYPE ) [ 0 ] || null ;
2026-05-03 09:49:29 +00:00
if ( ! leaf ) {
2026-05-03 10:44:44 +00:00
leaf = workspace . getLeaf ( "tab" ) ;
2026-06-04 10:46:05 +00:00
}
if ( leaf . view . getViewType ? . ( ) !== VIEW _TYPE ) {
2026-05-03 09:49:29 +00:00
await leaf . setViewState ( { type : VIEW _TYPE , active : true } ) ;
}
2026-05-03 12:52:40 +00:00
void workspace . revealLeaf ( leaf ) ;
2026-05-03 09:49:29 +00:00
const view = leaf . view ;
if ( view instanceof SmartFolderView ) {
view . setPinnedProfile ( profile || null ) ;
if ( persistLastActive && profile ? . id ) {
this . data . lastActiveProfileId = profile . id ;
await this . persist ( ) ;
}
2026-05-03 12:52:40 +00:00
view . onOpen ( ) ;
2026-05-03 09:49:29 +00:00
}
}
onunload ( ) {
}
async loadPluginData ( ) {
const raw = await this . loadData ( ) ;
this . data = {
2026-05-03 10:44:44 +00:00
language : raw ? . language === "zh" || raw ? . language === "en" ? raw . language : detectLanguage ( ) ,
languageMode : raw ? . languageMode === "auto" || raw ? . languageMode === "zh" || raw ? . languageMode === "en" ? raw . languageMode : "auto" ,
2026-05-03 09:49:29 +00:00
profiles : Array . isArray ( raw ? . profiles ) ? raw . profiles : [ ] ,
2026-05-03 10:44:44 +00:00
lastActiveProfileId : typeof raw ? . lastActiveProfileId === "string" ? raw . lastActiveProfileId : "" ,
2026-05-03 09:49:29 +00:00
colorConfig : {
2026-05-03 10:44:44 +00:00
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
}
2026-05-03 09:49:29 +00:00
} ;
this . draftProfile = null ;
}
async persist ( ) {
await this . saveData ( this . data ) ;
}
2026-05-03 10:44:44 +00:00
} ;
2026-05-03 09:49:29 +00:00
module . exports = SmartFolderPlugin ;