Add files components 406-420; Tree opens to a known choice
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.
This commit is contained in:
30 files changed
+2416
-2
No files matched your search
@@ -0,0 +1,173 @@
|
||||
package gallery
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/files"
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/icon"
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/input"
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
||||
)
|
||||
|
||||
var filesEntries = []Entry{
|
||||
{406, "FileExplorer", "文件管理器", "files", "Folders on the left, their files on the right. Double click opens.", demoFileExplorer},
|
||||
{407, "FileGrid", "文件网格", "files", "Big icons over short names. Arrows walk the grid; Enter opens.", demoFileGrid},
|
||||
{408, "FileIcon", "文件图标", "files", "Six kinds, placed dot by dot: folder, text, image, audio, code, archive.", demoFileIcon},
|
||||
{409, "PathBar", "路径栏", "files", "Where you are, folder by folder. Click one to climb back up.", demoPathBar},
|
||||
{410, "MemoryCard", "记忆卡", "files", "Fifteen blocks, like the grey card of old. DELETE frees one.", demoMemoryCard},
|
||||
{411, "DiskUsage", "磁盘占用", "files", "Where the room went, stacked in one bar.", demoDiskUsage},
|
||||
{412, "UploadQueue", "上传队列", "files", "Files on their way up. ✕ calls one back.", demoUploadQueue},
|
||||
{413, "Trash", "回收站", "files", "A bin that looks full when it is. EMPTY takes it out.", demoTrash},
|
||||
}
|
||||
|
||||
// disk is the demo's drive, from C: down.
|
||||
var disk = files.Dir{Name: "C:", Files: []files.File{{Name: "AUTOEXEC.BAT", Kind: files.Code, Size: 512}}, Dirs: []files.Dir{
|
||||
{Name: "GAMES", Dirs: []files.Dir{{Name: "HERO QUEST", Files: []files.File{
|
||||
{Name: "SAVE1.SAV", Kind: files.Archive, Size: 8192},
|
||||
{Name: "MAP.PNG", Kind: files.Image, Size: 48_000},
|
||||
{Name: "THEME.MID", Kind: files.Audio, Size: 21_504},
|
||||
{Name: "CHEATS.TXT", Kind: files.Text, Size: 1_337},
|
||||
}}}, Files: []files.File{{Name: "README.TXT", Kind: files.Text, Size: 2_048}}},
|
||||
{Name: "MUSIC", Files: []files.File{
|
||||
{Name: "BOSS FIGHT.MID", Kind: files.Audio, Size: 30_720},
|
||||
{Name: "CREDITS.MID", Kind: files.Audio, Size: 12_288},
|
||||
}},
|
||||
}}
|
||||
|
||||
// path is the demo path's folder names, root first.
|
||||
var path = []string{"C:", "GAMES", "HERO QUEST", "SAVES"}
|
||||
|
||||
type filesDemo struct {
|
||||
dir string
|
||||
file, grid, block, trash, at int
|
||||
saves []files.Save
|
||||
ups []files.Upload
|
||||
started time.Time
|
||||
}
|
||||
|
||||
func filesState(c *ui.Context) *filesDemo {
|
||||
return ui.Local(c.Root(), demoKey("files"), func() filesDemo {
|
||||
return filesDemo{dir: "0/0/0", file: -1, grid: -1, trash: 3, at: len(path) - 1, saves: []files.Save{
|
||||
{Title: "HERO QUEST", Icon: icon.Sword},
|
||||
{Title: "SPACE RACE", Icon: icon.Star, Slot: pixel.Warn},
|
||||
{Title: "MONSTER FARM", Icon: icon.Heart, Slot: pixel.Bad},
|
||||
{Title: "PUZZLE TOWER", Icon: icon.Gem, Slot: pixel.Magic},
|
||||
{Title: "FISHING DAY", Icon: icon.Drop, Slot: pixel.Info},
|
||||
}, ups: []files.Upload{
|
||||
{Name: "SPEEDRUN.ZIP", Kind: files.Archive, Size: 9 << 20},
|
||||
{Name: "BOSS.PNG", Kind: files.Image, Size: 3 << 20},
|
||||
{Name: "LEVEL 9.MAP", Kind: files.Code, Size: 1 << 20, Sent: 1 << 19, Failed: true},
|
||||
}}
|
||||
})
|
||||
}
|
||||
|
||||
func demoFileExplorer(c *ui.Context, log func(string)) {
|
||||
s := filesState(c)
|
||||
r := files.FileExplorer(c, "Explorer", disk, &s.dir, &s.file, files.FileExplorerOptions{})
|
||||
if r.Changed {
|
||||
log(fmt.Sprintf("folder %s, file %d", s.dir, s.file))
|
||||
}
|
||||
if r.Opened {
|
||||
log(fmt.Sprintf("open file %d", s.file))
|
||||
}
|
||||
}
|
||||
|
||||
func demoFileGrid(c *ui.Context, log func(string)) {
|
||||
s := filesState(c)
|
||||
all := slices.Concat(disk.Dirs[0].Dirs[0].Files, []files.File{{Name: "SECRET LEVEL", Kind: files.Folder}})
|
||||
r := files.FileGrid(c, "Files", all, &s.grid, files.FileGridOptions{Height: 150})
|
||||
if r.Changed {
|
||||
log("chose " + all[s.grid].Name)
|
||||
}
|
||||
if r.Opened {
|
||||
log("open " + all[s.grid].Name)
|
||||
}
|
||||
}
|
||||
|
||||
func demoFileIcon(c *ui.Context, log func(string)) {
|
||||
k := pixel.K(c)
|
||||
kinds := []files.Kind{files.Folder, files.Text, files.Image, files.Audio, files.Code, files.Archive}
|
||||
ui.Column(c).Gap(k.D(6)).Children(func() {
|
||||
for _, scale := range []int{1, 2} {
|
||||
ui.Row(c).Gap(k.D(6)).Children(func() {
|
||||
for _, kind := range kinds {
|
||||
files.FileIcon(c, kind, files.FileIconOptions{Scale: scale})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func demoPathBar(c *ui.Context, log func(string)) {
|
||||
s := filesState(c)
|
||||
// Climbing up trims the path; the reset walks back down.
|
||||
r := files.PathBar(c, "Path", path[:s.at+1], files.PathBarOptions{})
|
||||
if r.Clicked >= 0 {
|
||||
s.at = r.Clicked
|
||||
log("up to " + path[s.at])
|
||||
}
|
||||
if s.at < len(path)-1 && input.Button(c, "BACK DOWN", input.ButtonOptions{Size: input.Small}).Clicked {
|
||||
s.at = len(path) - 1
|
||||
log("down to " + path[s.at])
|
||||
}
|
||||
}
|
||||
|
||||
func demoMemoryCard(c *ui.Context, log func(string)) {
|
||||
s := filesState(c)
|
||||
r := files.MemoryCard(c, "Memory card 1", s.saves, &s.block, files.MemoryCardOptions{})
|
||||
if r.Changed {
|
||||
log(fmt.Sprintf("block %d", s.block+1))
|
||||
}
|
||||
if r.Deleted >= 0 {
|
||||
log("deleted " + s.saves[r.Deleted].Title)
|
||||
s.saves = append(s.saves[:r.Deleted:r.Deleted], s.saves[r.Deleted+1:]...)
|
||||
}
|
||||
}
|
||||
|
||||
func demoDiskUsage(c *ui.Context, log func(string)) {
|
||||
files.DiskUsage(c, "Disk", []files.Use{
|
||||
{Name: "GAMES", Bytes: 41 << 30}, {Name: "SCREENSHOTS", Bytes: 6 << 30},
|
||||
{Name: "MUSIC", Bytes: 12 << 30}, {Name: "SAVES", Bytes: 300 << 20},
|
||||
}, 128<<30, files.DiskUsageOptions{Width: 220})
|
||||
}
|
||||
|
||||
func demoUploadQueue(c *ui.Context, log func(string)) {
|
||||
k := pixel.K(c)
|
||||
s := filesState(c)
|
||||
if s.started.IsZero() {
|
||||
s.started = k.Now()
|
||||
}
|
||||
// The demo sends a megabyte a second, row by row.
|
||||
sent := int64(k.Since(c, s.started, 4, 60)) << 18
|
||||
ups := make([]files.Upload, len(s.ups))
|
||||
for i, u := range s.ups {
|
||||
if !u.Failed {
|
||||
u.Sent = min(u.Size, sent)
|
||||
sent = max(0, sent-u.Size)
|
||||
}
|
||||
ups[i] = u
|
||||
}
|
||||
if r := files.UploadQueue(c, "Uploads", ups, files.UploadQueueOptions{}); r.Canceled >= 0 {
|
||||
log("canceled " + s.ups[r.Canceled].Name)
|
||||
s.ups = append(s.ups[:r.Canceled:r.Canceled], s.ups[r.Canceled+1:]...)
|
||||
}
|
||||
}
|
||||
|
||||
func demoTrash(c *ui.Context, log func(string)) {
|
||||
k := pixel.K(c)
|
||||
s := filesState(c)
|
||||
ui.Column(c).Gap(k.D(6)).Children(func() {
|
||||
if files.Trash(c, "Trash", s.trash, files.TrashOptions{}).Emptied {
|
||||
log(fmt.Sprintf("emptied %d", s.trash))
|
||||
s.trash = 0
|
||||
}
|
||||
if input.Button(c, "THROW AWAY", input.ButtonOptions{Size: input.Small}).Clicked {
|
||||
s.trash++
|
||||
log(fmt.Sprintf("threw one away: %d", s.trash))
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package gallery
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/files"
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/input"
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
||||
)
|
||||
|
||||
var storageEntries = []Entry{
|
||||
{414, "RecentFiles", "最近文件", "files", "What you touched lately, and how long ago.", demoRecentFiles},
|
||||
{415, "FilePreview", "文件预览", "files", "Pictures shrink to fit, text scrolls, and the rest says so.", demoFilePreview},
|
||||
{416, "ArchiveViewer", "压缩包", "files", "What is inside the zip, and how much squeezing saved. Headers sort.", demoArchiveViewer},
|
||||
{417, "FileDetails", "文件详情", "files", "Name, kind, size to the byte, and when it all happened.", demoFileDetails},
|
||||
{418, "SharePanel", "分享面板", "files", "A link to send and how much it lets them do. COPY takes it.", demoSharePanel},
|
||||
{419, "SyncStatus", "同步状态", "files", "All saved, saving (the arrows turn), or something went wrong.", demoSyncStatus},
|
||||
{420, "QuotaMeter", "配额", "files", "Storage as gems, like hearts in an old adventure. Low turns gold, then red.", demoQuotaMeter},
|
||||
}
|
||||
|
||||
type storageDemo struct {
|
||||
recent, entry, kind int
|
||||
access files.Access
|
||||
}
|
||||
|
||||
func storageState(c *ui.Context) *storageDemo {
|
||||
return ui.Local(c.Root(), demoKey("storage"), func() storageDemo { return storageDemo{recent: -1, entry: -1} })
|
||||
}
|
||||
|
||||
func demoRecentFiles(c *ui.Context, log func(string)) {
|
||||
s := storageState(c)
|
||||
now := pixel.K(c).Now()
|
||||
recent := []files.File{
|
||||
{Name: "SAVE1.SAV", Kind: files.Archive, Modified: now.Add(-20 * time.Second)},
|
||||
{Name: "CHEATS.TXT", Kind: files.Text, Modified: now.Add(-12 * time.Minute)},
|
||||
{Name: "MAP.PNG", Kind: files.Image, Modified: now.Add(-5 * time.Hour)},
|
||||
{Name: "THEME.MID", Kind: files.Audio, Modified: now.Add(-30 * time.Hour)},
|
||||
{Name: "MAIN.GO", Kind: files.Code, Modified: now.Add(-9 * 24 * time.Hour)},
|
||||
}
|
||||
r := files.RecentFiles(c, "Recent", recent, &s.recent, files.RecentFilesOptions{})
|
||||
if r.Changed {
|
||||
log("chose " + recent[s.recent].Name)
|
||||
}
|
||||
if r.Opened {
|
||||
log("open " + recent[s.recent].Name)
|
||||
}
|
||||
}
|
||||
|
||||
const cheats = "UP UP DOWN DOWN LEFT RIGHT LEFT RIGHT B A START.\n\nWorks on the title screen. Thirty lives, or so the playground said. Hold SELECT for the level select, but only on Tuesdays."
|
||||
|
||||
func demoFilePreview(c *ui.Context, log func(string)) {
|
||||
k := pixel.K(c)
|
||||
s := storageState(c)
|
||||
ui.Column(c).Gap(k.D(6)).Children(func() {
|
||||
if input.Segmented(c, "File", []string{"CHEATS.TXT", "SUNSET.PNG", "THEME.MID"}, &s.kind, input.SegmentedOptions{}).Changed {
|
||||
log([]string{"text", "picture", "no preview"}[s.kind])
|
||||
}
|
||||
switch s.kind {
|
||||
case 0:
|
||||
files.FilePreview(c, "CHEATS.TXT", files.Preview{Text: cheats}, files.FilePreviewOptions{H: 80})
|
||||
case 1:
|
||||
files.FilePreview(c, "SUNSET.PNG", files.Preview{Picture: sunset}, files.FilePreviewOptions{H: 80})
|
||||
default:
|
||||
files.FilePreview(c, "THEME.MID", files.Preview{}, files.FilePreviewOptions{H: 80})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func demoArchiveViewer(c *ui.Context, log func(string)) {
|
||||
s := storageState(c)
|
||||
entries := []files.Entry{
|
||||
{Path: "LEVELS/1-1.MAP", Size: 18_432, Packed: 3_120},
|
||||
{Path: "LEVELS/1-2.MAP", Size: 20_480, Packed: 3_604},
|
||||
{Path: "MUSIC/THEME.MID", Size: 21_504, Packed: 19_800},
|
||||
{Path: "SPRITES.PNG", Size: 65_536, Packed: 64_900},
|
||||
{Path: "README.TXT", Size: 2_048, Packed: 900},
|
||||
}
|
||||
r := files.ArchiveViewer(c, "LOOT.ZIP", entries, &s.entry, files.ArchiveViewerOptions{Height: 80})
|
||||
if r.Changed {
|
||||
log("chose " + entries[s.entry].Path)
|
||||
}
|
||||
if r.Sorted {
|
||||
log("sorted")
|
||||
}
|
||||
}
|
||||
|
||||
func demoFileDetails(c *ui.Context, log func(string)) {
|
||||
files.FileDetails(c, files.File{
|
||||
Name: "BOSS FIGHT.PNG", Kind: files.Image, Size: 1_234_567,
|
||||
Created: time.Date(1998, 11, 21, 9, 30, 0, 0, time.UTC),
|
||||
Modified: time.Date(2026, 4, 1, 23, 59, 0, 0, time.UTC),
|
||||
}, files.FileDetailsOptions{})
|
||||
}
|
||||
|
||||
func demoSharePanel(c *ui.Context, log func(string)) {
|
||||
s := storageState(c)
|
||||
r := files.SharePanel(c, "Share", "https://8bit.example/s/UUDDLRLRBA", &s.access, files.SharePanelOptions{})
|
||||
if r.Copied {
|
||||
log("link copied")
|
||||
}
|
||||
if r.Changed {
|
||||
log([]string{"view", "comment", "edit"}[s.access])
|
||||
}
|
||||
}
|
||||
|
||||
func demoSyncStatus(c *ui.Context, log func(string)) {
|
||||
k := pixel.K(c)
|
||||
ui.Column(c).Gap(k.D(6)).Children(func() {
|
||||
for _, st := range []files.SyncState{files.Synced, files.Syncing, files.SyncFailed} {
|
||||
files.SyncStatus(c, st, files.SyncStatusOptions{})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func demoQuotaMeter(c *ui.Context, log func(string)) {
|
||||
k := pixel.K(c)
|
||||
ui.Column(c).Gap(k.D(8)).Children(func() {
|
||||
for _, used := range []int64{3 << 30, 13 << 30, 16 << 30} {
|
||||
files.QuotaMeter(c, "Storage", used, 15<<30, files.QuotaMeterOptions{})
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package gallery
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFilesDemosLogTheirActions(t *testing.T) {
|
||||
// Clicks that only focus a component may log nothing.
|
||||
quiet := map[string]bool{"FOLDERS": true}
|
||||
for _, tc := range []struct {
|
||||
path string
|
||||
steps []string
|
||||
}{
|
||||
{"/c/406-fileexplorer", []string{"FOLDERS", "key:Up"}}, {"/c/407-filegrid", []string{"SAVE1.SAV", "key:Right", "key:Enter"}},
|
||||
{"/c/409-pathbar", []string{"GAMES", "BACK DOWN"}}, {"/c/410-memorycard", []string{"DELETE HERO QUEST"}},
|
||||
{"/c/412-uploadqueue", []string{"CANCEL BOSS.PNG"}}, {"/c/413-trash", []string{"EMPTY", "THROW AWAY"}},
|
||||
{"/c/414-recentfiles", []string{"Recent", "key:Down", "key:Enter"}}, {"/c/415-filepreview", []string{"SUNSET.PNG"}},
|
||||
{"/c/416-archiveviewer", []string{"LOOT.ZIP", "key:Down", "SIZE"}}, {"/c/418-sharepanel", []string{"COPY", "EDIT"}},
|
||||
} {
|
||||
a, tt := open(tc.path)
|
||||
slug := tc.path[3:]
|
||||
for _, step := range tc.steps {
|
||||
before := len(a.logs[slug])
|
||||
if k, ok := strings.CutPrefix(step, "key:"); ok {
|
||||
tt.Key(0, letters[k])
|
||||
} else {
|
||||
click(t, tt, step)
|
||||
}
|
||||
if len(a.logs[slug]) <= before && !quiet[step] {
|
||||
t.Errorf("%s: %s logged nothing: %q", slug, step, a.logs[slug])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"github.com/egoist/mygo/ui"
|
||||
)
|
||||
|
||||
var letters = map[string]ui.Key{"A": ui.KeyA, "Right": ui.KeyRight, "Up": ui.KeyUp, "Enter": ui.KeyEnter}
|
||||
var letters = map[string]ui.Key{"A": ui.KeyA, "Right": ui.KeyRight, "Up": ui.KeyUp, "Down": ui.KeyDown, "Enter": ui.KeyEnter, "Tab": ui.KeyTab}
|
||||
|
||||
func TestMediaDemosLogTheirActions(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
|
||||
+1
-1
@@ -56,7 +56,7 @@ var Categories = []Category{
|
||||
{"tabletop", "Tabletop", "桌游", icon.Crown},
|
||||
}
|
||||
|
||||
var entries = slices.Concat(foundationEntries, layoutEntries, textEntries, displayEntries, inputEntries, controlEntries, feedbackEntries, navigationEntries, dataEntries, recordEntries, overlayEntries, popupEntries, datetimeEntries, timeEntries, chartEntries, plotEntries, gameEntries, rpgEntries, screenEntries, lootEntries, fxEntries, screenFxEntries, chatEntries, mailEntries, mediaEntries, studioEntries, codeEntries, debugEntries, accountEntries, playerEntries)
|
||||
var entries = slices.Concat(foundationEntries, layoutEntries, textEntries, displayEntries, inputEntries, controlEntries, feedbackEntries, navigationEntries, dataEntries, recordEntries, overlayEntries, popupEntries, datetimeEntries, timeEntries, chartEntries, plotEntries, gameEntries, rpgEntries, screenEntries, lootEntries, fxEntries, screenFxEntries, chatEntries, mailEntries, mediaEntries, studioEntries, codeEntries, debugEntries, accountEntries, playerEntries, filesEntries, storageEntries)
|
||||
|
||||
func Entries() []Entry { return entries }
|
||||
|
||||
|
||||
Reference in new issue
Block a user