Pad/internal/browser/types.go
Greg Pomerantz 5a92ee9df3 Fix: restore previous directory on navigation failure
When directory navigation fails (e.g., permission denied on Android),
the browser now restores the previous path instead of staying in a
broken empty state. NavigateTo saves the current path to History, and
handleError restores it and re-builds the index for the previous
directory when a BuildIndexTask fails.
2026-06-05 13:58:20 -04:00

157 lines
4.6 KiB
Go

package browser
import (
"fmt"
"time"
"gioui.org/widget"
"pad/internal/ui"
)
// Entry represents a single file or directory in the browser.
type Entry struct {
Path string // Relative path from configured directory
Name string // Display name (basename)
Size int64 // File size in bytes (0 for directories)
ModTime time.Time // Last modification time
IsDir bool // True if this is a directory
IsFirst bool // True if this is the first entry for its letter section
}
// Page represents a chunk of directory entries loaded from disk.
type Page struct {
Index int // Page number (0-based)
Entries []Entry // Entries in this page
Loaded bool // True if page data is in memory
Dirty bool // True if page needs refresh (external change detected)
}
// BrowserState holds all mutable browser state owned by the logic goroutine.
type BrowserState struct {
// Navigation
CurrentPath string // Currently browsed directory (relative to root)
History []string // Path history for navigation
ScrollOffset float64 // Vertical scroll offset in pixels (per-pixel scrolling)
EntryHeight float64 // Height of a single entry in pixels
VisibleCount int // Number of entries currently visible
// Computed from ScrollOffset and EntryHeight
scrollIndex int // Index of first visible entry (derived, not persisted)
// Lazy loading
Pages map[int]*Page // Loaded pages by page index
TotalEntries int // Total entry count (from cached index)
Loading bool // True if a page load is in flight
// Sorted index (position maps for each sort mode)
SortIndex *DirectoryIndex // Cached index with position maps
SortMode SortMode // Current sort mode
// Search
Query string // Current search query
SearchResults []int // Indices of matching entries (empty = no filter)
SearchEditor widget.Editor // Gio editor for search input
// Alphabetical index
ActiveLetter string // Currently pressed letter (for highlighting)
LetterOffsets map[string]int // First entry index for each letter
// Interaction
SelectedIndex int // Currently selected entry (-1 = none)
TapTimestamp time.Time // For double-tap detection
}
const (
PageSize = 100 // Entries per page (tunable; ~5KB per page in memory)
PrefetchDist = 2 // Pages to prefetch beyond visible region
)
// NewEntry creates a new Entry from the given parameters.
func NewEntry(path, name string, size int64, modTime time.Time, isDir bool) Entry {
return Entry{
Path: path,
Name: name,
Size: size,
ModTime: modTime,
IsDir: isDir,
}
}
// NewPage creates a new Page with the given index and entries.
func NewPage(index int, entries []Entry) *Page {
return &Page{
Index: index,
Entries: entries,
Loaded: true,
}
}
// NewBrowserState creates a new BrowserState with default values.
func NewBrowserState() *BrowserState {
return &BrowserState{
Pages: make(map[int]*Page),
LetterOffsets: make(map[string]int),
SelectedIndex: -1,
SortMode: SortModeDateDesc, // Default sort mode (newest first)
EntryHeight: 48.0,
CurrentPath: "/",
}
}
// ToListItem converts an Entry to a ui.ListItem for rendering.
func (e *Entry) ToListItem() ui.ListItem {
subtext := formatSize(e.Size)
if e.IsDir {
subtext = "Directory"
}
return ui.ListItem{
Text: e.Name,
Subtext: subtext,
}
}
// formatSize returns a human-readable file size string.
func formatSize(bytes int64) string {
switch {
case bytes < 1024:
return fmt.Sprintf("%d B", bytes)
case bytes < 1024*1024:
return fmt.Sprintf("%.1f KB", float64(bytes)/1024)
case bytes < 1024*1024*1024:
return fmt.Sprintf("%.1f MB", float64(bytes)/(1024*1024))
default:
return fmt.Sprintf("%.1f GB", float64(bytes)/(1024*1024*1024))
}
}
// EvictPages removes pages that are far from the current scroll position.
func (s *BrowserState) EvictPages() {
minPage := s.GetScrollIndex()/PageSize - PrefetchDist
maxPage := (s.GetScrollIndex() + s.VisibleCount)/PageSize + PrefetchDist
for idx, page := range s.Pages {
if idx < minPage || idx > maxPage {
page.Entries = nil // Release memory
page.Loaded = false
}
}
}
// GetScrollIndex returns the index of the first visible entry, derived from
// the per-pixel ScrollOffset and EntryHeight.
func (s *BrowserState) GetScrollIndex() int {
if s.EntryHeight <= 0 {
return 0
}
return int(s.ScrollOffset / s.EntryHeight)
}
// Reset clears all browser state for navigation.
func (s *BrowserState) Reset() {
s.CurrentPath = ""
s.Pages = make(map[int]*Page)
s.ScrollOffset = 0
s.SelectedIndex = -1
s.SearchResults = nil
s.Query = ""
}