@@ -3,6 +3,7 @@ import { ConfirmModal } from "./ConfirmModal";
33import { NextNoteSuggestModal } from "./NextNoteSuggestModal" ;
44import { getActiveFile , getPreviousNote , getNextNotes , getNextNotesWithCache , buildReverseCache , detachNote , setPreviousProperty , findLastNote , findFirstNote , isOnSamePath } from "./obsidian" ;
55import { CanvasGenerator , saveCanvasData } from "./canvas" ;
6+ import { ExportFilterModal } from "./ExportFilterModal" ;
67
78export async function goToPreviousNoteCommand ( app : App ) {
89 const file = getActiveFile ( app ) ;
@@ -350,3 +351,144 @@ export function exportAllRiversToCanvasCommand(app: App) {
350351 }
351352 ) . open ( ) ;
352353}
354+
355+ export function exportFilteredRiversToCanvasCommand ( app : App ) {
356+ new ExportFilterModal ( app , async ( result ) => {
357+ let { directory, tag, link, property, width, height, maxColumns, exportAll } = result ;
358+ if ( ! exportAll && ! directory && ! tag && ! link ) {
359+ new Notice ( "Please provide at least one filter criterion or check 'Search all elements'." ) ;
360+ return ;
361+ }
362+
363+ tag = tag . trim ( ) ;
364+ link = link . trim ( ) ;
365+ directory = directory . trim ( ) ;
366+ property = property ?. trim ( ) ;
367+
368+ if ( tag && ! tag . startsWith ( "#" ) ) {
369+ tag = "#" + tag ;
370+ }
371+
372+ const allFiles = app . vault . getMarkdownFiles ( ) ;
373+ let matchedFiles : TFile [ ] = [ ] ;
374+
375+ if ( exportAll ) {
376+ matchedFiles = allFiles ;
377+ } else {
378+ for ( const file of allFiles ) {
379+ let match = true ;
380+
381+ if ( directory && ! file . path . includes ( directory ) ) {
382+ match = false ;
383+ }
384+
385+ if ( match && ( tag || link ) ) {
386+ const cache = app . metadataCache . getFileCache ( file ) ;
387+
388+ if ( tag ) {
389+ let allTags : string [ ] = [ ] ;
390+
391+ if ( property ) {
392+ const propertyValue = cache ?. frontmatter ?. [ property ] ;
393+ let propertyTags : string [ ] = [ ] ;
394+ if ( Array . isArray ( propertyValue ) ) {
395+ propertyTags = propertyValue . map ( t => String ( t ) . trim ( ) ) ;
396+ } else if ( typeof propertyValue === 'string' ) {
397+ propertyTags = propertyValue . split ( "," ) . map ( t => t . trim ( ) ) ;
398+ }
399+ allTags = propertyTags . map ( t => t . startsWith ( "#" ) ? t : "#" + t ) ;
400+ } else {
401+ const fileTags = cache ?. tags ?. map ( t => t . tag ) || [ ] ;
402+ const frontmatterTagsFromCache = cache ?. frontmatter ?. tags ;
403+
404+ let fmTags : string [ ] = [ ] ;
405+ if ( Array . isArray ( frontmatterTagsFromCache ) ) {
406+ fmTags = frontmatterTagsFromCache ;
407+ } else if ( typeof frontmatterTagsFromCache === 'string' ) {
408+ fmTags = frontmatterTagsFromCache . split ( "," ) . map ( t => t . trim ( ) ) ;
409+ }
410+
411+ allTags = [ ...fileTags , ...fmTags . map ( t => t . startsWith ( "#" ) ? t : "#" + t ) ] ;
412+ }
413+
414+ const hasTag = allTags . some ( t => t === tag || t . startsWith ( tag + "/" ) ) ;
415+ if ( ! hasTag ) match = false ;
416+ }
417+
418+ if ( match && link ) {
419+ if ( property ) {
420+ const fileFrontmatterLinks = cache ?. frontmatterLinks ?. filter ( l => l . key === property ) || [ ] ;
421+ const hasLink = fileFrontmatterLinks . some ( l => l . link . includes ( link ) ) ;
422+ if ( ! hasLink ) match = false ;
423+ } else {
424+ const fileLinks = cache ?. links ?. map ( l => l . link ) || [ ] ;
425+ const fileEmbeds = cache ?. embeds ?. map ( e => e . link ) || [ ] ;
426+ const fileFrontmatterLinks = cache ?. frontmatterLinks ?. map ( l => l . link ) || [ ] ;
427+ const allLinks = [ ...fileLinks , ...fileEmbeds , ...fileFrontmatterLinks ] ;
428+ const hasLink = allLinks . some ( l => l . includes ( link ) ) ;
429+ if ( ! hasLink ) match = false ;
430+ }
431+ }
432+ }
433+
434+ if ( match ) {
435+ matchedFiles . push ( file ) ;
436+ }
437+ }
438+ }
439+
440+ if ( matchedFiles . length === 0 ) {
441+ new Notice ( "No files matched the given criteria." ) ;
442+ return ;
443+ }
444+
445+ const reverseCache = buildReverseCache ( app ) ;
446+
447+ const roots = new Set < TFile > ( ) ;
448+ for ( const file of matchedFiles ) {
449+ const prev = getPreviousNote ( app , file ) ;
450+ const nexts = getNextNotesWithCache ( app , file , reverseCache ) ;
451+
452+ // Skip isolated notes that do not belong to any river
453+ if ( ! prev && nexts . length === 0 ) {
454+ continue ;
455+ }
456+
457+ const root = findFirstNote ( app , file ) ;
458+ roots . add ( root ) ;
459+ }
460+
461+ const numWidth = parseInt ( width ) ;
462+ const numHeight = parseInt ( height ) ;
463+ const numMaxColumns = parseInt ( maxColumns ) ;
464+
465+ const generatorOptions = {
466+ width : isNaN ( numWidth ) ? 400 : numWidth ,
467+ height : isNaN ( numHeight ) ? 500 : numHeight ,
468+ maxColumns : isNaN ( numMaxColumns ) ? 5 : numMaxColumns
469+ } ;
470+
471+ const generator = new CanvasGenerator ( app , reverseCache , generatorOptions ) ;
472+ let currentY = 0 ;
473+
474+ for ( const root of roots ) {
475+ if ( generator . fileToNodeId . has ( root . path ) ) continue ;
476+
477+ generator . dfs ( root , 0 , currentY , 1 ) ;
478+ currentY = generator . maxUsedY + 1000 ;
479+ }
480+
481+ if ( generator . nodes . length === 0 ) {
482+ new Notice ( "No connected notes found for the matched files." ) ;
483+ return ;
484+ }
485+
486+ const activeFile = getActiveFile ( app ) ;
487+ const sourcePath = activeFile ? activeFile . path : "" ;
488+ const canvasName = "Filtered Connected Notes.canvas" ;
489+ const saveDir = app . fileManager . getNewFileParent ( sourcePath , "Filtered Connected Notes.md" ) . path ;
490+
491+ await saveCanvasData ( app , generator . nodes , generator . edges , canvasName , saveDir ) ;
492+
493+ } ) . open ( ) ;
494+ }
0 commit comments