Fifteen file components: FileExplorer, FileGrid, FileIcon, PathBar, MemoryCard, DiskUsage, UploadQueue, Trash, RecentFiles, FilePreview, ArchiveViewer, FileDetails, SharePanel, SyncStatus and QuotaMeter. FileIcon draws six kinds as hand-placed 12x12 pages and a folder. The browsers build on data.Tree, List, GridView and Table; ArchiveViewer owns its sort and keeps the caller's index on its entry. UploadQueue keys rows by name so focus stays on its row. SharePanel never logs the link. SyncStatus turns its arrows on Busy steps, so syncing moves under Reduce Motion. data.Tree now opens the closed branches above a chosen path it knows, so a caller's starting path survives the first frame.
102 lines
2.9 KiB
Go
102 lines
2.9 KiB
Go
package files
|
|
|
|
import (
|
|
"fmt"
|
|
"log/slog"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/egoist/mygo/ui"
|
|
|
|
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/data"
|
|
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
|
)
|
|
|
|
// Dir is a named folder of folders and files.
|
|
type Dir struct {
|
|
Name string
|
|
Dirs []Dir
|
|
Files []File
|
|
}
|
|
|
|
type FileExplorerOptions struct {
|
|
// Height is the file list's in dots; zero means 120.
|
|
Height int
|
|
Disabled bool
|
|
}
|
|
|
|
type FileExplorerResult struct {
|
|
Element *ui.Element
|
|
// Opened is Enter or a double click on a file.
|
|
Changed, Opened bool
|
|
}
|
|
|
|
// FileExplorer sets the folder tree beside the chosen folder's files.
|
|
func FileExplorer(c *ui.Context, text string, root Dir, path *string, file *int, o FileExplorerOptions) FileExplorerResult {
|
|
k := pixel.K(c)
|
|
dir, ok := folderAt(root, *path)
|
|
if !ok {
|
|
panic(fmt.Sprintf("files: FileExplorer has no folder at %q", *path))
|
|
}
|
|
pick("FileExplorer", file, len(dir.Files))
|
|
h := pixel.Or("FileExplorer Height", o.Height, 120)
|
|
tree, tw := node(root, 0)
|
|
var res FileExplorerResult
|
|
res.Element = ui.Row(c).FillWidth().Gap(k.D(4)).AlignItems(ui.Start).Role(ui.RoleGroup).Label(text).Children(func() {
|
|
old := *path
|
|
data.Tree(c, k.Msg("folders"), []data.TreeNode{tree}, path, data.TreeOptions{Width: tw, Disabled: o.Disabled})
|
|
if *path != old {
|
|
// A new folder starts with no file chosen.
|
|
dir, _ = folderAt(root, *path)
|
|
*file, res.Changed = -1, true
|
|
slog.Debug("8bit: file explorer folder", "label", text, "path", *path)
|
|
}
|
|
files := dir.Files
|
|
for _, f := range files {
|
|
check("FileExplorer", f)
|
|
}
|
|
l := data.List(c, dir.Name, len(files), file, data.ListOptions{Height: h, Disabled: o.Disabled}, func(i int) string { return files[i].Name }, func(i int) {
|
|
row(c, k, files[i], func() { pixel.Text(c, size(files[i].Size), pixel.TextOptions{Slot: pixel.Faint}) })
|
|
})
|
|
l.Element.Grow(1)
|
|
res.Changed = res.Changed || l.Changed
|
|
res.Opened = l.Submitted && *file >= 0
|
|
})
|
|
if res.Opened {
|
|
slog.Debug("8bit: file explorer open", "label", text, "file", *file)
|
|
}
|
|
return res
|
|
}
|
|
|
|
// node makes a folder's tree and the width it needs.
|
|
func node(f Dir, depth int) (data.TreeNode, int) {
|
|
if f.Name == "" {
|
|
panic("files: FileExplorer has a folder with no name")
|
|
}
|
|
// data.Tree indents ten dots a level; the arrow takes twelve.
|
|
n, w := data.TreeNode{Label: f.Name}, depth*10+16+pixel.Measure(f.Name)
|
|
for _, sub := range f.Dirs {
|
|
child, cw := node(sub, depth+1)
|
|
n.Children, w = append(n.Children, child), max(w, cw)
|
|
}
|
|
return n, w
|
|
}
|
|
|
|
// folderAt follows a tree path, "0/1", down from root.
|
|
func folderAt(root Dir, path string) (Dir, bool) {
|
|
parts := strings.Split(path, "/")
|
|
if parts[0] != "0" {
|
|
return Dir{}, false
|
|
}
|
|
f := root
|
|
for _, p := range parts[1:] {
|
|
// Only the plain form, as data.Tree writes it, is known.
|
|
i, err := strconv.Atoi(p)
|
|
if err != nil || strconv.Itoa(i) != p || i < 0 || i >= len(f.Dirs) {
|
|
return Dir{}, false
|
|
}
|
|
f = f.Dirs[i]
|
|
}
|
|
return f, true
|
|
}
|