···4546// LocalImageResolver resolves local file paths to image metadata
47type LocalImageResolver struct {
48- // BlobUploader is called to upload image bytes and get a blob reference
49- // If nil, creates a placeholder blob with a hash-based CID
50- //
51- // TODO: CLI commands that publish documents must provide this function to upload
52- // images to AT Protocol blob storage via com.atproto.repo.uploadBlob
53 BlobUploader func(data []byte, mimeType string) (Blob, error)
54}
55
···4546// LocalImageResolver resolves local file paths to image metadata
47type LocalImageResolver struct {
48+ // Called to upload image bytes and get a blob reference
000049 BlobUploader func(data []byte, mimeType string) (Blob, error)
50}
51
+30-29
internal/services/atproto.go
···4// - Pull: Fetch pub.leaflet.document records from AT Protocol repository
5// - Post: Create new pub.leaflet.document records in AT Protocol repository
6// - Push: Update existing pub.leaflet.document records in AT Protocol repository
7-//
8-// Publishing Workflow (TODO):
9-// 1. Post - Create new document:
10-// - Convert note to pub.leaflet.document format
11-// - Upload any embedded images as blobs
12-// - Create record with com.atproto.repo.createRecord
13-// - Store returned rkey and cid in note metadata
14-// 2. Push - Update existing document:
15-// - Check if note has leaflet_rkey (indicates previously published)
16-// - Convert updated note to pub.leaflet.document format
17-// - Upload any new images as blobs
18-// - Update record with com.atproto.repo.putRecord
19-// - Update stored cid in note metadata
20-// 3. Delete - Remove published document:
21-// - Use com.atproto.repo.deleteRecord with stored rkey
22-// - Clear leaflet metadata from note
23-//
24-// Blob Upload (TODO):
25-// 1. Use com.atproto.repo.uploadBlob for images
26-// 2. Returns blob reference with CID
27-// 3. Include blob ref in ImageBlock structures
28-//
29-// Draft vs Published (TODO):
30-// 1. Draft documents stored in collection: pub.leaflet.document.draft
31-// 2. Published documents in: pub.leaflet.document
32-// 3. Moving from draft to published requires:
33-// - Delete from draft collection
34-// - Create in published collection
35-// - Update note metadata with new rkey
36package services
3738import (
···467 }
468469 return nil
00000000000000000000000000000470}
471472// Close cleans up resources
···4// - Pull: Fetch pub.leaflet.document records from AT Protocol repository
5// - Post: Create new pub.leaflet.document records in AT Protocol repository
6// - Push: Update existing pub.leaflet.document records in AT Protocol repository
7+// - Delete: Remove pub.leaflet.document records from AT Protocol repository
00000000000000000000000000008package services
910import (
···439 }
440441 return nil
442+}
443+444+// UploadBlob uploads binary data as a blob to AT Protocol
445+func (s *ATProtoService) UploadBlob(ctx context.Context, data []byte, mimeType string) (public.Blob, error) {
446+ if !s.IsAuthenticated() {
447+ return public.Blob{}, fmt.Errorf("not authenticated")
448+ }
449+450+ if len(data) == 0 {
451+ return public.Blob{}, fmt.Errorf("data cannot be empty")
452+ }
453+454+ if mimeType == "" {
455+ return public.Blob{}, fmt.Errorf("mimeType is required")
456+ }
457+458+ output, err := atproto.RepoUploadBlob(ctx, s.client, bytes.NewReader(data))
459+ if err != nil {
460+ return public.Blob{}, fmt.Errorf("failed to upload blob: %w", err)
461+ }
462+463+ blob := public.Blob{
464+ Type: public.TypeBlob,
465+ Ref: public.CID{Link: output.Blob.Ref.String()},
466+ MimeType: output.Blob.MimeType,
467+ Size: int(output.Blob.Size),
468+ }
469+470+ return blob, nil
471}
472473// Close cleans up resources
+3-10
internal/ui/note_list_adapter.go
···44}
4546func (n *NoteRecord) GetDescription() string {
47- // Create a short description from tags and modification time
48 var parts []string
4950 if len(n.Tags) > 0 {
···80 repoOpts.Archived = &archived
81 }
8283- // Apply search filter if provided
84 if opts.Search != "" {
85- repoOpts.Content = opts.Search // Search in content
86 }
8788 if opts.Limit > 0 {
···103}
104105func (n *NoteDataSource) Count(ctx context.Context, opts ListOptions) (int, error) {
106- // For simplicity, load all and count (could be optimized with a separate Count method)
107 items, err := n.Load(ctx, opts)
108 if err != nil {
109 return 0, err
···112}
113114func (n *NoteDataSource) Search(ctx context.Context, query string, opts ListOptions) ([]ListItem, error) {
115- // Set search in options and use regular Load
116 opts.Search = query
117 return n.Load(ctx, opts)
118}
···123 opts.Title = "Notes"
124 }
125126- // Enable search functionality for notes
127 opts.ShowSearch = true
128 opts.Searchable = true
129130- // Set up view handler for markdown rendering
131 if opts.ViewHandler == nil {
132 opts.ViewHandler = func(item ListItem) string {
133 if noteRecord, ok := item.(*NoteRecord); ok {
···188 }
189 }
190191- // Render markdown
192 renderer, err := glamour.NewTermRenderer(
193 glamour.WithAutoStyle(),
194 glamour.WithWordWrap(80),
195 )
196 if err != nil {
197- return content.String() // Return unrendered if glamour fails
198 }
199200 rendered, err := renderer.Render(content.String())
201 if err != nil {
202- return content.String() // Return unrendered if rendering fails
203 }
204205 return rendered