cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists 🍃
charm leaflet readability golang
at main 210 lines 4.8 kB view raw
1package ui 2 3import ( 4 "context" 5 "fmt" 6 "io" 7 "strings" 8 9 "github.com/stormlightlabs/noteleaf/internal/models" 10 "github.com/stormlightlabs/noteleaf/internal/repo" 11 "github.com/stormlightlabs/noteleaf/internal/utils" 12) 13 14// BookRecord adapts models.Book to work with DataList 15type BookRecord struct { 16 *models.Book 17} 18 19func (b *BookRecord) GetField(name string) any { 20 switch name { 21 case "id": 22 return b.ID 23 case "title": 24 return b.Title 25 case "author": 26 return b.Author 27 case "status": 28 return b.Status 29 case "progress": 30 return b.Progress 31 case "pages": 32 return b.Pages 33 case "rating": 34 return b.Rating 35 case "notes": 36 return b.Notes 37 case "added": 38 return b.Added 39 case "started": 40 return b.Started 41 case "finished": 42 return b.Finished 43 default: 44 return "" 45 } 46} 47 48func (b *BookRecord) GetTitle() string { 49 return b.Title 50} 51 52func (b *BookRecord) GetDescription() string { 53 var parts []string 54 55 if b.Author != "" { 56 parts = append(parts, "by "+b.Author) 57 } 58 59 if b.Status != "" { 60 parts = append(parts, utils.Titlecase(b.Status)) 61 } 62 63 if b.Pages > 0 { 64 parts = append(parts, fmt.Sprintf("%d pages", b.Pages)) 65 } 66 67 if b.Progress > 0 && b.Progress < 100 { 68 parts = append(parts, fmt.Sprintf("%d%%", b.Progress)) 69 } 70 71 return strings.Join(parts, " • ") 72} 73 74func (b *BookRecord) GetFilterValue() string { 75 // Make books searchable by title, author, and notes 76 searchable := []string{b.Title, b.Author, b.Notes} 77 return strings.Join(searchable, " ") 78} 79 80// BookDataSource adapts BookRepository to work with DataList 81type BookDataSource struct { 82 repo utils.TestBookRepository 83 status string 84} 85 86func (b *BookDataSource) Load(ctx context.Context, opts ListOptions) ([]ListItem, error) { 87 repoOpts := repo.BookListOptions{} 88 89 if b.status != "" { 90 repoOpts.Status = b.status 91 } 92 93 if opts.Search != "" { 94 // Simple search in title/author (could be enhanced) 95 repoOpts.Search = opts.Search 96 } 97 98 if opts.Limit > 0 { 99 repoOpts.Limit = opts.Limit 100 } 101 102 books, err := b.repo.List(ctx, repoOpts) 103 if err != nil { 104 return nil, err 105 } 106 107 items := make([]ListItem, len(books)) 108 for i, book := range books { 109 items[i] = &BookRecord{Book: book} 110 } 111 112 return items, nil 113} 114 115func (b *BookDataSource) Count(ctx context.Context, opts ListOptions) (int, error) { 116 // For simplicity, load all and count (could be optimized) 117 items, err := b.Load(ctx, opts) 118 if err != nil { 119 return 0, err 120 } 121 return len(items), nil 122} 123 124func (b *BookDataSource) Search(ctx context.Context, query string, opts ListOptions) ([]ListItem, error) { 125 // Set search in options and use regular Load 126 opts.Search = query 127 return b.Load(ctx, opts) 128} 129 130// formatBookForView formats a book for detailed viewing 131func formatBookForView(book *models.Book) string { 132 var content strings.Builder 133 134 content.WriteString(fmt.Sprintf("# %s\n\n", book.Title)) 135 136 if book.Author != "" { 137 content.WriteString(fmt.Sprintf("**Author:** %s\n", book.Author)) 138 } 139 140 if book.Status != "" { 141 content.WriteString(fmt.Sprintf("**Status:** %s\n", utils.Titlecase(book.Status))) 142 } 143 144 if book.Progress > 0 { 145 content.WriteString(fmt.Sprintf("**Progress:** %d%%\n", book.Progress)) 146 } 147 148 if book.Pages > 0 { 149 content.WriteString(fmt.Sprintf("**Pages:** %d\n", book.Pages)) 150 } 151 152 if book.Rating > 0 { 153 content.WriteString(fmt.Sprintf("**Rating:** %.1f/5\n", book.Rating)) 154 } 155 156 content.WriteString(fmt.Sprintf("**Added:** %s\n", book.Added.Format("2006-01-02 15:04"))) 157 158 if book.Started != nil { 159 content.WriteString(fmt.Sprintf("**Started:** %s\n", book.Started.Format("2006-01-02 15:04"))) 160 } 161 162 if book.Finished != nil { 163 content.WriteString(fmt.Sprintf("**Finished:** %s\n", book.Finished.Format("2006-01-02 15:04"))) 164 } 165 166 if book.Notes != "" { 167 content.WriteString(fmt.Sprintf("\n**Notes:**\n%s\n", book.Notes)) 168 } 169 170 return content.String() 171} 172 173// NewBookDataList creates a new DataList for browsing books 174func NewBookDataList(repo utils.TestBookRepository, opts DataListOptions, status string) *DataList { 175 if opts.Title == "" { 176 opts.Title = "Books" 177 } 178 179 // Enable search functionality 180 opts.ShowSearch = true 181 opts.Searchable = true 182 183 // Set up view handler for book details 184 if opts.ViewHandler == nil { 185 opts.ViewHandler = func(item ListItem) string { 186 if bookRecord, ok := item.(*BookRecord); ok { 187 return formatBookForView(bookRecord.Book) 188 } 189 return "Unable to display book" 190 } 191 } 192 193 source := &BookDataSource{ 194 repo: repo, 195 status: status, 196 } 197 198 return NewDataList(source, opts) 199} 200 201// NewBookListFromList creates a BookList-compatible interface using DataList 202func NewBookListFromList(repo utils.TestBookRepository, output io.Writer, input io.Reader, static bool, status string) *DataList { 203 opts := DataListOptions{ 204 Output: output, 205 Input: input, 206 Static: static, 207 Title: "Books", 208 } 209 return NewBookDataList(repo, opts, status) 210}