Merge branch 'worktree-agent-a9950b6fffa6f4a57'
This commit is contained in:
96 files changed
+6468
-16
No files matched your search
@@ -1,3 +1,23 @@
|
||||
package main
|
||||
|
||||
var filesPages = []page{}
|
||||
const filesCategory = "Files & Collaboration"
|
||||
|
||||
var filesPages = []page{
|
||||
{ID: "381", Name: "FileExplorer", ZhName: "文件浏览器", Category: filesCategory, Demo: fileExplorerDemo, Code: filesFileExplorerCode, Keys: "Tab 切换;方向键选择;Enter 打开;macOS Command+[ / ] / ↑,其他平台 Alt+← / → / ↑。"},
|
||||
{ID: "382", Name: "FileGrid", ZhName: "文件网格", Category: filesCategory, Demo: fileGridDemo, Code: filesFileGridCode, Keys: "方向键选择;Command/Ctrl+点击多选;Shift 扩展;Enter 打开。"},
|
||||
{ID: "383", Name: "FileIcon", ZhName: "文件图标", Category: filesCategory, Demo: fileIconDemo, Code: filesFileIconCode, Keys: "展示组件,沿用父级焦点。"},
|
||||
{ID: "384", Name: "FilePreview", ZhName: "文件预览", Category: filesCategory, Demo: filePreviewDemo, Code: filesFilePreviewCode, Keys: "Escape 关闭;Tab 到操作;文本支持选择与复制。"},
|
||||
{ID: "385", Name: "PathBar", ZhName: "路径栏", Category: filesCategory, Demo: pathBarDemo, Code: filesPathBarCode, Keys: "Tab 进入;左右移动;Enter 激活;Escape 关闭折叠菜单。"},
|
||||
{ID: "386", Name: "RenameInline", ZhName: "行内重命名", Category: filesCategory, Demo: renameInlineDemo, Code: filesRenameInlineCode, Keys: "Enter 确认;Escape 取消;双击开始编辑;失焦确认有效名称。"},
|
||||
{ID: "387", Name: "FileOperationProgress", ZhName: "文件操作进度", Category: filesCategory, Demo: fileOperationProgressDemo, Code: filesFileOperationProgressCode, Keys: "Tab 到取消、全选规则和冲突选项;Enter / Space 激活。"},
|
||||
{ID: "388", Name: "TransferQueue", ZhName: "传输队列", Category: filesCategory, Demo: transferQueueDemo, Code: filesTransferQueueCode, Keys: "Tab 在可见行操作间移动;Enter / Space 激活。"},
|
||||
{ID: "389", Name: "StorageUsage", ZhName: "存储占用", Category: filesCategory, Demo: storageUsageDemo, Code: filesStorageUsageCode, Keys: "Tab 聚焦分段条;左右循环读取分类与剩余空间。"},
|
||||
{ID: "390", Name: "RecentFiles", ZhName: "最近文件", Category: filesCategory, Demo: recentFilesDemo, Code: filesRecentFilesCode, Keys: "上下选择文件;Enter 打开;Tab 到清除和移除按钮。"},
|
||||
{ID: "391", Name: "ArchiveViewer", ZhName: "压缩包浏览", Category: filesCategory, Demo: archiveViewerDemo, Code: filesArchiveViewerCode, Keys: "上下选择;左右展开折叠;Enter 打开;Tab 到列操作。"},
|
||||
{ID: "392", Name: "PresenceAvatars", ZhName: "协作头像", Category: filesCategory, Demo: presenceAvatarsDemo, Code: filesPresenceAvatarsCode, Keys: "Tab 进入;左右移动;Enter 激活;Escape 关闭溢出名单。"},
|
||||
{ID: "394", Name: "Reactions", ZhName: "表情回应", Category: filesCategory, Demo: reactionsDemo, Code: filesReactionsCode, Keys: "Tab 到回应与添加;Enter / Space 激活;选择器左右导航;Escape 关闭。"},
|
||||
{ID: "395", Name: "ShareDialog", ZhName: "分享对话框", Category: filesCategory, Demo: shareDialogDemo, Code: filesShareDialogCode, Keys: "Tab 在对话框内移动;Enter 邀请或激活操作;Escape 关闭并恢复焦点。"},
|
||||
{ID: "396", Name: "PermissionSelect", ZhName: "权限选择", Category: filesCategory, Demo: permissionSelectDemo, Code: filesPermissionSelectCode, Keys: "Enter 打开;上下移动;Enter 确认;Escape 关闭。"},
|
||||
{ID: "399", Name: "LiveIndicator", ZhName: "同步状态", Category: filesCategory, Demo: liveIndicatorDemo, Code: filesLiveIndicatorCode, Keys: "Tab 到重试;Enter / Space 激活。"},
|
||||
{ID: "400", Name: "RemoteCursor", ZhName: "远程光标", Category: filesCategory, Demo: remoteCursorDemo, Code: filesRemoteCursorCode, Keys: "输入穿透至下方编辑器。"},
|
||||
}
|
||||
@@ -0,0 +1,362 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
mujica "mujicaui"
|
||||
)
|
||||
|
||||
var filesFolders = map[string][]string{
|
||||
"/court": {"chronicles/", "decrees/", "seal.go", "notes.md"},
|
||||
"/court/chronicles": {"first.md", "second.md"},
|
||||
"/court/decrees": {"42.pdf", "crest.png"},
|
||||
}
|
||||
|
||||
func filesEntries(dir string) []mujica.FileEntry {
|
||||
var entries []mujica.FileEntry
|
||||
for _, n := range filesFolders[dir] {
|
||||
name := strings.TrimSuffix(n, "/")
|
||||
entries = append(entries, mujica.FileEntry{Path: path.Join(dir, name), Name: name, Dir: name != n, Size: 2048})
|
||||
}
|
||||
return entries
|
||||
}
|
||||
|
||||
func fileExplorerDemo(c *ui.Context, log func(string)) {
|
||||
type state struct {
|
||||
explorer mujica.FileExplorerState
|
||||
disabled, empty bool
|
||||
}
|
||||
s := ui.Local(c.Root(), "381", func() state { return state{} })
|
||||
mediaCheck(c, log, &s.disabled, "Disabled")
|
||||
mediaCheck(c, log, &s.empty, "Empty folder")
|
||||
r := mujica.FileExplorer(c, &s.explorer, mujica.FileExplorerOptions{
|
||||
Roots: []string{"/court"}, Disabled: s.disabled,
|
||||
Folders: func(dir string) []string {
|
||||
var dirs []string
|
||||
for _, e := range filesEntries(dir) {
|
||||
if e.Dir {
|
||||
dirs = append(dirs, e.Path)
|
||||
}
|
||||
}
|
||||
return dirs
|
||||
},
|
||||
Entries: func(dir string) []mujica.FileEntry {
|
||||
if s.empty {
|
||||
return nil
|
||||
}
|
||||
return filesEntries(dir)
|
||||
},
|
||||
})
|
||||
r.Element.Height(320).FillWidth()
|
||||
if r.Navigated {
|
||||
log("Navigate: " + s.explorer.Current)
|
||||
}
|
||||
if r.Changed {
|
||||
log(fmt.Sprintf("Selected: %v", s.explorer.Selected()))
|
||||
}
|
||||
if r.Opened != nil {
|
||||
log("Open: " + r.Opened.Path)
|
||||
}
|
||||
}
|
||||
|
||||
func fileGridDemo(c *ui.Context, log func(string)) {
|
||||
type state struct {
|
||||
grid mujica.FileGridState
|
||||
entries []mujica.FileEntry
|
||||
disabled, thumbs, empty bool
|
||||
}
|
||||
s := ui.Local(c.Root(), "382", func() state {
|
||||
return state{thumbs: true, entries: append(filesEntries("/court"), filesEntries("/court/decrees")...)}
|
||||
})
|
||||
mediaCheck(c, log, &s.disabled, "Disabled")
|
||||
mediaCheck(c, log, &s.thumbs, "Thumbnails")
|
||||
mediaCheck(c, log, &s.empty, "Empty")
|
||||
entries := s.entries
|
||||
if s.empty {
|
||||
entries = nil
|
||||
}
|
||||
r := mujica.FileGrid(c, entries, &s.grid, mujica.FileGridOptions{Disabled: s.disabled, Thumbnail: func(e mujica.FileEntry) bool {
|
||||
if !s.thumbs || mujica.FileKindOf(e.Name) != mujica.FileImage {
|
||||
return false
|
||||
}
|
||||
mujica.Image(c, mediaBitmaps[0], mujica.ImageOptions{Alt: e.Name}).Element.Fill()
|
||||
return true
|
||||
}})
|
||||
r.Element.Height(320).FillWidth()
|
||||
if r.Changed {
|
||||
log(fmt.Sprintf("Selected: %v", s.grid.Selected()))
|
||||
}
|
||||
if r.Opened != nil {
|
||||
log("Open: " + r.Opened.Path)
|
||||
}
|
||||
}
|
||||
|
||||
func fileIconDemo(c *ui.Context, log func(string)) {
|
||||
type state struct {
|
||||
name string
|
||||
folder, open bool
|
||||
size float64
|
||||
}
|
||||
s := ui.Local(c.Root(), "383", func() state { return state{name: "seal.go", size: 32} })
|
||||
if mujica.TextInput(c, &s.name, mujica.TextInputOptions{Label: "File name"}).Changed() {
|
||||
log("File name changed")
|
||||
}
|
||||
mediaCheck(c, log, &s.folder, "Folder")
|
||||
if s.folder {
|
||||
mediaCheck(c, log, &s.open, "Open folder")
|
||||
}
|
||||
if mujica.NumberInput(c, &s.size, mujica.NumberInputOptions{Min: 20, Max: 64, Step: 4, Label: "Icon size"}).Changed() {
|
||||
log(fmt.Sprintf("Size: %.0f", s.size))
|
||||
}
|
||||
mujica.FileIcon(c, s.name, mujica.FileIconOptions{Folder: s.folder, Open: s.folder && s.open, Size: float32(s.size)})
|
||||
ui.Row(c).Gap(12).Children(func() {
|
||||
for _, name := range []string{"seal.go", "notes.md", "crest.png", "deed.pdf", "ledger.zip", "hymn.mp3", "rite.mov", "blob"} {
|
||||
mujica.FileIcon(c, name, mujica.FileIconOptions{Size: 32})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func filePreviewDemo(c *ui.Context, log func(string)) {
|
||||
type state struct {
|
||||
kind int
|
||||
status mujica.DataStatus
|
||||
closed bool
|
||||
}
|
||||
s := ui.Local(c.Root(), "384", func() state { return state{} })
|
||||
if mujica.Select(c, &s.kind, []mujica.SelectOption[int]{{Value: 0, Label: "Code"}, {Value: 1, Label: "Text"}, {Value: 2, Label: "Image"}, {Value: 3, Label: "Unsupported"}}, mujica.SelectOptions{Label: "Preview type"}).Changed() {
|
||||
log("Preview type changed")
|
||||
s.closed = false
|
||||
}
|
||||
filesStatus(c, log, &s.status)
|
||||
if s.closed {
|
||||
if mujica.Button(c, "Show preview", mujica.ButtonOptions{}).Clicked() {
|
||||
s.closed = false
|
||||
log("Preview reopened")
|
||||
}
|
||||
return
|
||||
}
|
||||
f := []mujica.PreviewFile{{Name: "seal.go", Text: "package court\n\nfunc Seal() {}"}, {Name: "notes.md", Text: "The seal of the court."}, {Name: "crest.png", Image: mediaBitmaps[0]}, {Name: "deed.pdf", Size: 4096}}[s.kind]
|
||||
r := mujica.FilePreview(c, f, mujica.FilePreviewOptions{Status: s.status, Error: "Reading failed"})
|
||||
r.Element.Height(280).FillWidth()
|
||||
if r.Closed {
|
||||
s.closed = true
|
||||
log("Preview closed")
|
||||
}
|
||||
if r.Opened {
|
||||
log("Open: " + f.Name)
|
||||
}
|
||||
if r.Retried {
|
||||
s.status = mujica.DataReady
|
||||
log("Read retried")
|
||||
}
|
||||
}
|
||||
|
||||
func filesStatus(c *ui.Context, log func(string), value *mujica.DataStatus) {
|
||||
if mujica.Select(c, value, []mujica.SelectOption[mujica.DataStatus]{{Value: mujica.DataReady, Label: "Ready"}, {Value: mujica.DataLoading, Label: "Loading"}, {Value: mujica.DataFailed, Label: "Failed"}}, mujica.SelectOptions{Label: "Load status"}).Changed() {
|
||||
log(fmt.Sprintf("Status: %d", *value))
|
||||
}
|
||||
}
|
||||
|
||||
func pathBarDemo(c *ui.Context, log func(string)) {
|
||||
type state struct {
|
||||
path string
|
||||
disabled bool
|
||||
}
|
||||
s := ui.Local(c.Root(), "385", func() state { return state{path: "/court/chronicles/royal/archives/seals"} })
|
||||
mediaCheck(c, log, &s.disabled, "Disabled")
|
||||
if mujica.TextInput(c, &s.path, mujica.TextInputOptions{Label: "Path"}).Changed() {
|
||||
log("Path changed")
|
||||
}
|
||||
if strings.Trim(s.path, "/") == "" {
|
||||
ui.Text(c, "Enter a path")
|
||||
return
|
||||
}
|
||||
if r := mujica.PathBar(c, s.path, mujica.PathBarOptions{Disabled: s.disabled}); r.Navigated != "" {
|
||||
s.path = r.Navigated
|
||||
log("Navigate: " + r.Navigated)
|
||||
}
|
||||
}
|
||||
|
||||
func renameInlineDemo(c *ui.Context, log func(string)) {
|
||||
type state struct {
|
||||
name string
|
||||
editing, disabled, folder bool
|
||||
}
|
||||
s := ui.Local(c.Root(), "386", func() state { return state{name: "seal.txt"} })
|
||||
mediaCheck(c, log, &s.disabled, "Disabled")
|
||||
mediaCheck(c, log, &s.folder, "Folder")
|
||||
if mujica.Button(c, "Rename", mujica.ButtonOptions{Disabled: s.disabled}).Clicked() {
|
||||
s.editing = true
|
||||
log("Rename started")
|
||||
}
|
||||
r := mujica.RenameInline(c, &s.name, &s.editing, mujica.RenameInlineOptions{Disabled: s.disabled, Folder: s.folder, Validate: func(name string) string {
|
||||
if strings.ContainsAny(name, "/\\") {
|
||||
return "Name contains a path separator"
|
||||
}
|
||||
return ""
|
||||
}})
|
||||
if r.Renamed {
|
||||
log("Renamed: " + s.name)
|
||||
}
|
||||
if r.Canceled {
|
||||
log("Rename canceled")
|
||||
}
|
||||
}
|
||||
|
||||
func fileOperationProgressDemo(c *ui.Context, log func(string)) {
|
||||
type state struct {
|
||||
op mujica.FileOperation
|
||||
disabled, conflict bool
|
||||
canceled bool
|
||||
}
|
||||
s := ui.Local(c.Root(), "387", func() state {
|
||||
return state{op: mujica.FileOperation{Kind: mujica.FileCopy, Total: 10, Current: "seal.go"}}
|
||||
})
|
||||
mediaCheck(c, log, &s.disabled, "Disabled")
|
||||
mediaCheck(c, log, &s.conflict, "Conflict")
|
||||
if mujica.Select(c, &s.op.Kind, []mujica.SelectOption[mujica.FileOperationKind]{{Value: mujica.FileCopy, Label: "Copy"}, {Value: mujica.FileMove, Label: "Move"}, {Value: mujica.FileDelete, Label: "Delete"}}, mujica.SelectOptions{Label: "Operation"}).Changed() {
|
||||
log("Operation changed")
|
||||
}
|
||||
if mujica.Button(c, "Advance", mujica.ButtonOptions{Disabled: s.op.Done == s.op.Total}).Clicked() {
|
||||
s.op.Done++
|
||||
log(fmt.Sprintf("Done: %d", s.op.Done))
|
||||
}
|
||||
if mujica.Button(c, "Reset", mujica.ButtonOptions{}).Clicked() {
|
||||
s.op.Done = 0
|
||||
s.canceled = false
|
||||
log("Operation reset")
|
||||
}
|
||||
op := s.op
|
||||
if s.conflict {
|
||||
op.Conflict = "seal.go"
|
||||
}
|
||||
r := mujica.FileOperationProgress(c, op, mujica.FileOperationOptions{Disabled: s.disabled || s.canceled})
|
||||
if r.Canceled {
|
||||
s.canceled = true
|
||||
log("Operation canceled")
|
||||
}
|
||||
if r.Resolved != mujica.ConflictNone {
|
||||
s.conflict = false
|
||||
log(fmt.Sprintf("Conflict: %d; apply all: %v", r.Resolved, r.ApplyAll))
|
||||
}
|
||||
}
|
||||
|
||||
func filesTransfers() []mujica.QueuedTransfer {
|
||||
return []mujica.QueuedTransfer{
|
||||
{ID: "a", Name: "ledger.zip", Upload: true, Status: mujica.TransferActive, Done: 30, Total: 120},
|
||||
{ID: "b", Name: "crest.png", Status: mujica.TransferPaused, Done: 1, Total: 4},
|
||||
{ID: "c", Name: "deed.pdf", Upload: true, Status: mujica.TransferFailed, Total: 2, Error: "Network lost"},
|
||||
}
|
||||
}
|
||||
|
||||
func transferQueueDemo(c *ui.Context, log func(string)) {
|
||||
type state struct {
|
||||
items []mujica.QueuedTransfer
|
||||
queue mujica.TransferQueueState
|
||||
disabled bool
|
||||
revision uint64
|
||||
}
|
||||
s := ui.Local(c.Root(), "388", func() state { return state{items: filesTransfers()} })
|
||||
mediaCheck(c, log, &s.disabled, "Disabled")
|
||||
if mujica.Button(c, "Reset", mujica.ButtonOptions{}).Clicked() {
|
||||
s.items = filesTransfers()
|
||||
log("Queue reset")
|
||||
}
|
||||
r := mujica.TransferQueue(c, s.items, &s.queue, mujica.TransferQueueOptions{Disabled: s.disabled, Revision: s.revision})
|
||||
r.Element.Height(260).FillWidth()
|
||||
if r.Action == mujica.TransferNone {
|
||||
return
|
||||
}
|
||||
for i := range s.items {
|
||||
if s.items[i].ID == r.ID {
|
||||
switch r.Action {
|
||||
case mujica.TransferPause:
|
||||
s.items[i].Status = mujica.TransferPaused
|
||||
case mujica.TransferResume, mujica.TransferRetry:
|
||||
s.items[i].Status = mujica.TransferActive
|
||||
case mujica.TransferRemove:
|
||||
s.items = append(s.items[:i:i], s.items[i+1:]...)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
s.revision++
|
||||
log(fmt.Sprintf("Transfer %s: %d", r.ID, r.Action))
|
||||
}
|
||||
|
||||
func storageUsageDemo(c *ui.Context, log func(string)) {
|
||||
type state struct{ photos float64 }
|
||||
s := ui.Local(c.Root(), "389", func() state { return state{photos: 40} })
|
||||
if mujica.NumberInput(c, &s.photos, mujica.NumberInputOptions{Min: 0, Max: 92, Step: 1, Label: "Photos (GB)"}).Changed() {
|
||||
log(fmt.Sprintf("Photos: %.0f GB", s.photos))
|
||||
}
|
||||
mujica.StorageUsage(c, []mujica.StorageSegment{{Label: "Documents", Bytes: 24 << 30}, {Label: "Photos", Bytes: int64(s.photos) * (1 << 30)}, {Label: "Apps", Bytes: 12 << 30}}, mujica.StorageUsageOptions{Capacity: 128 << 30, Label: "Court disk"}).FillWidth()
|
||||
}
|
||||
|
||||
func filesRecent() []mujica.RecentFile {
|
||||
now := time.Date(2026, 10, 5, 15, 0, 0, 0, time.UTC)
|
||||
return []mujica.RecentFile{{Path: "/court/seal.go", Opened: now}, {Path: "/court/notes.md", Opened: now.AddDate(0, 0, -1)}, {Path: "/court/crest.png", Opened: now.AddDate(0, 0, -4)}, {Path: "/court/charter.pdf", Opened: now.AddDate(0, -1, 0)}}
|
||||
}
|
||||
|
||||
func recentFilesDemo(c *ui.Context, log func(string)) {
|
||||
type state struct {
|
||||
files []mujica.RecentFile
|
||||
recent mujica.RecentFilesState
|
||||
disabled bool
|
||||
}
|
||||
s := ui.Local(c.Root(), "390", func() state { return state{files: filesRecent()} })
|
||||
mediaCheck(c, log, &s.disabled, "Disabled")
|
||||
if mujica.Button(c, "Reset", mujica.ButtonOptions{}).Clicked() {
|
||||
s.files = filesRecent()
|
||||
log("Recent files reset")
|
||||
}
|
||||
r := mujica.RecentFiles(c, s.files, &s.recent, mujica.RecentFilesOptions{Disabled: s.disabled, Now: time.Date(2026, 10, 5, 15, 0, 0, 0, time.UTC)})
|
||||
r.Element.Height(320).FillWidth()
|
||||
if r.Opened != nil {
|
||||
log("Open: " + r.Opened.Path)
|
||||
}
|
||||
if r.Cleared {
|
||||
s.files = nil
|
||||
log("Recent files cleared")
|
||||
}
|
||||
if r.Removed != "" {
|
||||
for i, f := range s.files {
|
||||
if f.Path == r.Removed {
|
||||
s.files = append(s.files[:i:i], s.files[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
log("Removed: " + r.Removed)
|
||||
}
|
||||
}
|
||||
|
||||
func archiveViewerDemo(c *ui.Context, log func(string)) {
|
||||
type state struct {
|
||||
archive mujica.ArchiveViewerState
|
||||
entries []mujica.ArchiveEntry
|
||||
status mujica.DataStatus
|
||||
empty bool
|
||||
}
|
||||
s := ui.Local(c.Root(), "391", func() state {
|
||||
return state{entries: []mujica.ArchiveEntry{{Path: "court/seal.go", Size: 4000, Packed: 1000}, {Path: "court/decrees/42.pdf", Size: 6000, Packed: 5000}, {Path: "README.md", Size: 1000, Packed: 400}}}
|
||||
})
|
||||
filesStatus(c, log, &s.status)
|
||||
mediaCheck(c, log, &s.empty, "Empty")
|
||||
entries := s.entries
|
||||
if s.empty {
|
||||
entries = nil
|
||||
}
|
||||
r := mujica.ArchiveViewer(c, entries, &s.archive, mujica.ArchiveViewerOptions{Status: s.status, Error: "Invalid archive"})
|
||||
r.Element.Height(280).FillWidth()
|
||||
if r.Opened != nil {
|
||||
log("Open archive entry: " + r.Opened.Path)
|
||||
}
|
||||
if r.Retried {
|
||||
s.status = mujica.DataReady
|
||||
log("Archive read retried")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package main
|
||||
|
||||
const filesFileExplorerCode = `var state mujica.FileExplorerState
|
||||
opts := mujica.FileExplorerOptions{
|
||||
Roots: []string{"/court"},
|
||||
Folders: func(string) []string { return nil },
|
||||
Entries: func(string) []mujica.FileEntry {
|
||||
return []mujica.FileEntry{{Path: "/court/seal.go", Name: "seal.go", Size: 1024}}
|
||||
},
|
||||
}
|
||||
view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
mujica.FileExplorer(c, &state, opts).Element.Height(240)
|
||||
}
|
||||
ui.Render(view, 600, 300, 1)`
|
||||
|
||||
const filesFileGridCode = `entries := []mujica.FileEntry{{Path: "/court/seal.go", Name: "seal.go"}}
|
||||
var state mujica.FileGridState
|
||||
view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
mujica.FileGrid(c, entries, &state, mujica.FileGridOptions{}).Element.Height(240)
|
||||
}
|
||||
ui.Render(view, 400, 300, 1)`
|
||||
|
||||
const filesFileIconCode = `view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
mujica.FileIcon(c, "seal.go", mujica.FileIconOptions{Size: 32})
|
||||
}
|
||||
ui.Render(view, 100, 80, 1)`
|
||||
|
||||
const filesFilePreviewCode = `file := mujica.PreviewFile{Name: "seal.go", Text: "package court\nfunc Seal() {}"}
|
||||
view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
mujica.FilePreview(c, file, mujica.FilePreviewOptions{}).Element.Height(240)
|
||||
}
|
||||
ui.Render(view, 500, 300, 1)`
|
||||
|
||||
const filesPathBarCode = `view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
mujica.PathBar(c, "/court/chronicles/seals", mujica.PathBarOptions{})
|
||||
}
|
||||
ui.Render(view, 400, 100, 1)`
|
||||
|
||||
const filesRenameInlineCode = `name, editing := "seal.txt", true
|
||||
view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
mujica.RenameInline(c, &name, &editing, mujica.RenameInlineOptions{})
|
||||
}
|
||||
ui.Render(view, 400, 100, 1)`
|
||||
|
||||
const filesFileOperationProgressCode = `op := mujica.FileOperation{Kind: mujica.FileCopy, Done: 2, Total: 10, Current: "seal.go"}
|
||||
view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
mujica.FileOperationProgress(c, op, mujica.FileOperationOptions{})
|
||||
}
|
||||
ui.Render(view, 500, 240, 1)`
|
||||
|
||||
const filesTransferQueueCode = `items := []mujica.QueuedTransfer{{ID: "a", Name: "ledger.zip", Upload: true, Status: mujica.TransferActive, Done: 25, Total: 100}}
|
||||
var state mujica.TransferQueueState
|
||||
view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
mujica.TransferQueue(c, items, &state, mujica.TransferQueueOptions{}).Element.Height(240)
|
||||
}
|
||||
ui.Render(view, 500, 300, 1)`
|
||||
|
||||
const filesStorageUsageCode = `segments := []mujica.StorageSegment{{Label: "Documents", Bytes: 24 << 30}, {Label: "Photos", Bytes: 40 << 30}}
|
||||
view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
mujica.StorageUsage(c, segments, mujica.StorageUsageOptions{Capacity: 128 << 30, Label: "Court disk"})
|
||||
}
|
||||
ui.Render(view, 500, 240, 1)`
|
||||
|
||||
const filesRecentFilesCode = `now := time.Date(2026, 10, 5, 12, 0, 0, 0, time.UTC)
|
||||
files := []mujica.RecentFile{{Path: "/court/seal.go", Opened: now}}
|
||||
var state mujica.RecentFilesState
|
||||
view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
mujica.RecentFiles(c, files, &state, mujica.RecentFilesOptions{Now: now}).Element.Height(240)
|
||||
}
|
||||
ui.Render(view, 500, 300, 1)`
|
||||
|
||||
const filesArchiveViewerCode = `entries := []mujica.ArchiveEntry{{Path: "court/seal.go", Size: 4000, Packed: 1000}}
|
||||
var state mujica.ArchiveViewerState
|
||||
view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
mujica.ArchiveViewer(c, entries, &state, mujica.ArchiveViewerOptions{}).Element.Height(240)
|
||||
}
|
||||
ui.Render(view, 600, 300, 1)`
|
||||
|
||||
const filesPresenceAvatarsCode = `people := []mujica.Collaborator{{ID: "ada", Name: "Ada Lovelace"}}
|
||||
view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
mujica.PresenceAvatars(c, people, mujica.PresenceAvatarsOptions{})
|
||||
}
|
||||
ui.Render(view, 300, 100, 1)`
|
||||
|
||||
const filesReactionsCode = `items := []mujica.Reaction{{Emoji: "\U0001f44d", Count: 3, Mine: true}}
|
||||
view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
mujica.Reactions(c, items, mujica.ReactionsOptions{Choices: []string{"\U0001f44d", "\U0001f389"}})
|
||||
}
|
||||
ui.Render(view, 400, 100, 1)`
|
||||
|
||||
const filesShareDialogCode = `open := false
|
||||
members := []mujica.ShareMember{{ID: "ada", Name: "Ada", Permission: mujica.PermissionEdit, Owner: true}}
|
||||
view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
if mujica.Button(c, "Share", mujica.ButtonOptions{}).Clicked() {
|
||||
open = true
|
||||
}
|
||||
mujica.ShareDialog(c, &open, &members, mujica.ShareDialogOptions{Title: "Ledger", Link: "https://court.org/ledger"})
|
||||
}
|
||||
ui.Render(view, 600, 400, 1)`
|
||||
|
||||
const filesPermissionSelectCode = `value := mujica.PermissionView
|
||||
view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
mujica.PermissionSelect(c, &value, mujica.PermissionSelectOptions{})
|
||||
}
|
||||
ui.Render(view, 300, 100, 1)`
|
||||
|
||||
const filesLiveIndicatorCode = `view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
mujica.LiveIndicator(c, mujica.LiveOffline, mujica.LiveIndicatorOptions{})
|
||||
}
|
||||
ui.Render(view, 500, 100, 1)`
|
||||
|
||||
const filesRemoteCursorCode = `carets := []mujica.RemoteCaret{{ID: "ada", Name: "Ada", Offset: 4, Anchor: 4}}
|
||||
view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
ui.Box(c).Size(300, 100).Children(func() {
|
||||
mujica.RemoteCursor(c, carets, mujica.RemoteCursorOptions{
|
||||
Caret: func(offset int) ui.Rect { return ui.Rect{X: float32(offset) * 8, Y: 20, H: 20} },
|
||||
})
|
||||
})
|
||||
}
|
||||
ui.Render(view, 320, 120, 1)`
|
||||
@@ -0,0 +1,195 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
mujica "mujicaui"
|
||||
"mujicaui/theme"
|
||||
)
|
||||
|
||||
var filesPeople = []mujica.Collaborator{
|
||||
{ID: "a", Name: "Ada Lovelace"}, {ID: "b", Name: "Blaise Pascal", Away: true},
|
||||
{ID: "c", Name: "Carl Gauss"}, {ID: "d", Name: "Dora Maar"}, {ID: "e", Name: "Emmy Noether"},
|
||||
}
|
||||
|
||||
func presenceAvatarsDemo(c *ui.Context, log func(string)) {
|
||||
type state struct {
|
||||
following string
|
||||
disabled, empty bool
|
||||
max float64
|
||||
}
|
||||
s := ui.Local(c.Root(), "392", func() state { return state{max: 3} })
|
||||
mediaCheck(c, log, &s.disabled, "Disabled")
|
||||
mediaCheck(c, log, &s.empty, "Empty")
|
||||
if mujica.NumberInput(c, &s.max, mujica.NumberInputOptions{Min: 1, Max: 5, Step: 1, Label: "Visible avatars"}).Changed() {
|
||||
log(fmt.Sprintf("Visible: %.0f", s.max))
|
||||
}
|
||||
people := filesPeople
|
||||
if s.empty {
|
||||
people = nil
|
||||
}
|
||||
r := mujica.PresenceAvatars(c, people, mujica.PresenceAvatarsOptions{Following: s.following, Max: int(s.max), Disabled: s.disabled})
|
||||
if r.Follow != "" {
|
||||
s.following = r.Follow
|
||||
log("Follow: " + r.Follow)
|
||||
}
|
||||
if r.Unfollow {
|
||||
s.following = ""
|
||||
log("Following stopped")
|
||||
}
|
||||
}
|
||||
|
||||
var filesReactionChoices = []string{"\U0001f44d", "\u2764\ufe0f", "\U0001f389", "\U0001f440", "\U0001f64f"}
|
||||
|
||||
func reactionsDemo(c *ui.Context, log func(string)) {
|
||||
type state struct {
|
||||
items []mujica.Reaction
|
||||
disabled bool
|
||||
}
|
||||
s := ui.Local(c.Root(), "394", func() state {
|
||||
return state{items: []mujica.Reaction{{Emoji: filesReactionChoices[0], Count: 3, Mine: true}, {Emoji: filesReactionChoices[2], Count: 1}}}
|
||||
})
|
||||
mediaCheck(c, log, &s.disabled, "Disabled")
|
||||
if mujica.Button(c, "Clear reactions", mujica.ButtonOptions{}).Clicked() {
|
||||
s.items = nil
|
||||
log("Reactions cleared")
|
||||
}
|
||||
r := mujica.Reactions(c, s.items, mujica.ReactionsOptions{Choices: filesReactionChoices, Disabled: s.disabled})
|
||||
emoji := r.Toggled
|
||||
if emoji == "" {
|
||||
emoji = r.Added
|
||||
}
|
||||
if emoji == "" {
|
||||
return
|
||||
}
|
||||
for i := range s.items {
|
||||
if s.items[i].Emoji == emoji {
|
||||
if s.items[i].Mine {
|
||||
s.items[i].Mine = false
|
||||
s.items[i].Count--
|
||||
} else {
|
||||
s.items[i].Mine = true
|
||||
s.items[i].Count++
|
||||
}
|
||||
if s.items[i].Count == 0 {
|
||||
s.items = append(s.items[:i:i], s.items[i+1:]...)
|
||||
}
|
||||
log("Reaction toggled")
|
||||
return
|
||||
}
|
||||
}
|
||||
s.items = append(s.items, mujica.Reaction{Emoji: emoji, Count: 1, Mine: true})
|
||||
log("Reaction added")
|
||||
}
|
||||
|
||||
func shareDialogDemo(c *ui.Context, log func(string)) {
|
||||
type state struct {
|
||||
members []mujica.ShareMember
|
||||
open, disabled bool
|
||||
next int
|
||||
}
|
||||
s := ui.Local(c.Root(), "395", func() state {
|
||||
return state{members: []mujica.ShareMember{
|
||||
{ID: "owner", Name: "Ada Lovelace", Email: "ada@court.org", Owner: true, Permission: mujica.PermissionEdit},
|
||||
{ID: "b", Name: "Blaise Pascal", Email: "blaise@court.org", Permission: mujica.PermissionView},
|
||||
}}
|
||||
})
|
||||
mediaCheck(c, log, &s.disabled, "Disabled")
|
||||
if mujica.Button(c, "Share Ledger", mujica.ButtonOptions{Disabled: s.disabled}).Clicked() {
|
||||
s.open = true
|
||||
log("Share dialog opened")
|
||||
}
|
||||
was := s.open
|
||||
r := mujica.ShareDialog(c, &s.open, &s.members, mujica.ShareDialogOptions{Title: "Ledger", Link: "https://court.org/ledger", Disabled: s.disabled, OnCopy: func(link string) { c.WriteClipboard(link); log("Copy callback") }})
|
||||
if r.Invited != nil {
|
||||
s.next++
|
||||
s.members = append(s.members, mujica.ShareMember{ID: fmt.Sprintf("invite-%d", s.next), Name: r.Invited.Email, Email: r.Invited.Email, Permission: r.Invited.Permission})
|
||||
log("Member invited")
|
||||
}
|
||||
if r.Changed != "" {
|
||||
log("Permission changed: " + r.Changed)
|
||||
}
|
||||
if r.Removed != "" {
|
||||
log("Member removed: " + r.Removed)
|
||||
}
|
||||
if r.Copied {
|
||||
log("Link copied")
|
||||
}
|
||||
if was && !s.open {
|
||||
log("Share dialog closed")
|
||||
}
|
||||
}
|
||||
|
||||
func permissionSelectDemo(c *ui.Context, log func(string)) {
|
||||
type state struct {
|
||||
value mujica.Permission
|
||||
disabled bool
|
||||
}
|
||||
s := ui.Local(c.Root(), "396", func() state { return state{} })
|
||||
mediaCheck(c, log, &s.disabled, "Disabled")
|
||||
if mujica.PermissionSelect(c, &s.value, mujica.PermissionSelectOptions{Disabled: s.disabled}).Changed() {
|
||||
log(fmt.Sprintf("Permission: %d", s.value))
|
||||
}
|
||||
}
|
||||
|
||||
func liveIndicatorDemo(c *ui.Context, log func(string)) {
|
||||
type state struct {
|
||||
status mujica.LiveStatus
|
||||
showTime bool
|
||||
}
|
||||
s := ui.Local(c.Root(), "399", func() state { return state{showTime: true} })
|
||||
if mujica.Select(c, &s.status, []mujica.SelectOption[mujica.LiveStatus]{{Value: mujica.LiveSynced, Label: "Synced"}, {Value: mujica.LiveSyncing, Label: "Syncing"}, {Value: mujica.LiveReconnecting, Label: "Reconnecting"}, {Value: mujica.LiveOffline, Label: "Offline"}, {Value: mujica.LiveError, Label: "Error"}}, mujica.SelectOptions{Label: "Sync status"}).Changed() {
|
||||
log(fmt.Sprintf("Sync status: %d", s.status))
|
||||
}
|
||||
mediaCheck(c, log, &s.showTime, "Last sync time")
|
||||
now := time.Date(2026, 10, 5, 12, 0, 0, 0, time.UTC)
|
||||
opts := mujica.LiveIndicatorOptions{Error: "Server unavailable", Now: now}
|
||||
if s.showTime {
|
||||
opts.LastSynced = now.Add(-3 * time.Minute)
|
||||
}
|
||||
if mujica.LiveIndicator(c, s.status, opts).Changed() {
|
||||
s.status = mujica.LiveReconnecting
|
||||
log("Sync retried")
|
||||
}
|
||||
}
|
||||
|
||||
func remoteCursorDemo(c *ui.Context, log func(string)) {
|
||||
type state struct {
|
||||
offset float64
|
||||
selection, second bool
|
||||
}
|
||||
s := ui.Local(c.Root(), "400", func() state { return state{offset: 48, selection: true, second: true} })
|
||||
if mujica.NumberInput(c, &s.offset, mujica.NumberInputOptions{Min: 0, Max: 79, Step: 1, Label: "Remote text offset"}).Changed() {
|
||||
log(fmt.Sprintf("Offset: %.0f", s.offset))
|
||||
}
|
||||
mediaCheck(c, log, &s.selection, "Selection")
|
||||
mediaCheck(c, log, &s.second, "Second collaborator")
|
||||
carets := []mujica.RemoteCaret{{ID: "ada", Name: "Ada", Offset: int(s.offset), Anchor: int(s.offset)}}
|
||||
if s.selection {
|
||||
carets[0].Anchor = max(0, carets[0].Offset-5)
|
||||
}
|
||||
if s.second {
|
||||
carets = append(carets, mujica.RemoteCaret{ID: "blaise", Name: "Blaise", Offset: 8, Anchor: 8})
|
||||
}
|
||||
ui.Box(c).Height(140).FillWidth().Background(mujica.Tokens(c).Surface).Children(func() {
|
||||
ui.Column(c).Children(func() {
|
||||
for _, line := range []string{"The seal of the court is kept", "by the chancellor, who answers"} {
|
||||
ui.Text(c, line).Font(theme.MonoFont).FontSize(mujica.FontSize(c, 13)).Height(20)
|
||||
}
|
||||
})
|
||||
at := func(o int) (float32, float32) { return float32(o%40) * 8, float32(o/40) * 20 }
|
||||
mujica.RemoteCursor(c, carets, mujica.RemoteCursorOptions{
|
||||
Caret: func(o int) ui.Rect { x, y := at(o); return ui.Rect{X: x, Y: y, H: 20} },
|
||||
Selection: func(from, to int) []ui.Rect {
|
||||
var out []ui.Rect
|
||||
for o := from; o < to; o++ {
|
||||
x, y := at(o)
|
||||
out = append(out, ui.Rect{X: x, Y: y, W: 8, H: 20})
|
||||
}
|
||||
return out
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
mujica "mujicaui"
|
||||
)
|
||||
|
||||
func filesDemoTester(t *testing.T, id string, mode mujica.Mode) (*ui.Tester, *[]string) {
|
||||
t.Helper()
|
||||
for _, p := range filesPages {
|
||||
if p.ID == id {
|
||||
events := &[]string{}
|
||||
tt := ui.NewTester(func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{Mode: mode})
|
||||
ui.Scroll(c).Fill().Padding(24).Gap(12).Children(func() { p.Demo(c, func(s string) { *events = append(*events, s) }) })
|
||||
}, 900, 800)
|
||||
tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1})
|
||||
tt.Frame()
|
||||
return tt, events
|
||||
}
|
||||
}
|
||||
t.Fatalf("missing files page %s", id)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func TestFilesGalleryExistingSet(t *testing.T) {
|
||||
want := []string{"381", "382", "383", "384", "385", "386", "387", "388", "389", "390", "391", "392", "394", "395", "396", "399", "400"}
|
||||
if len(filesPages) != len(want) {
|
||||
t.Fatalf("files pages %d, want %d", len(filesPages), len(want))
|
||||
}
|
||||
for i, p := range filesPages {
|
||||
if p.ID != want[i] || p.Category != "Files & Collaboration" || p.Code == "" || p.Keys == "" {
|
||||
t.Fatalf("page %s has incorrect metadata", p.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilesGalleryActions(t *testing.T) {
|
||||
cases := []struct{ id, action, event string }{
|
||||
{"381", "Disabled", "Disabled = true"},
|
||||
{"382", "Empty", "Empty = true"},
|
||||
{"383", "Folder", "Folder = true"},
|
||||
{"384", "Close preview", "Preview closed"},
|
||||
{"385", "court", "Navigate: /court"},
|
||||
{"386", "Rename", "Rename started"},
|
||||
{"387", "Cancel", "Operation canceled"},
|
||||
{"388", "Pause ledger.zip", "Transfer a: 1"},
|
||||
{"390", "Clear", "Recent files cleared"},
|
||||
{"391", "Empty", "Empty = true"},
|
||||
{"392", "Follow Ada Lovelace", "Follow: a"},
|
||||
{"394", "Clear reactions", "Reactions cleared"},
|
||||
{"395", "Share Ledger", "Share dialog opened"},
|
||||
{"396", "Disabled", "Disabled = true"},
|
||||
{"399", "Last sync time", "Last sync time = false"},
|
||||
{"400", "Selection", "Selection = false"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
for _, mode := range []mujica.Mode{mujica.Light, mujica.Dark} {
|
||||
t.Run(fmt.Sprintf("%s-%s", tc.id, mode), func(t *testing.T) {
|
||||
tt, events := filesDemoTester(t, tc.id, mode)
|
||||
if err := tt.Click(tc.action); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
found := false
|
||||
for _, event := range *events {
|
||||
if event == tc.event {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("events %v, want %s", *events, tc.event)
|
||||
}
|
||||
if tc.id == "395" {
|
||||
if err := tt.Click("Copy link"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if tt.Clipboard() != "https://court.org/ledger" {
|
||||
t.Fatal("share callback did not copy link")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilesGalleryStorageParameter(t *testing.T) {
|
||||
tt, events := filesDemoTester(t, "389", mujica.Light)
|
||||
if err := tt.Click("Photos (GB)"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tt.Key(ui.Cmd, ui.KeyA)
|
||||
tt.Type("41")
|
||||
tt.Key(0, ui.KeyEnter)
|
||||
if !tt.HasText("77.0 GB of 128.0 GB used") || len(*events) == 0 {
|
||||
t.Fatalf("storage parameter left %q; events %v", tt.Texts(), *events)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilesGalleryGridRange(t *testing.T) {
|
||||
tt, events := filesDemoTester(t, "382", mujica.Light)
|
||||
if err := tt.Click("notes.md"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tt.Frame()
|
||||
if err := tt.ClickWith(ui.Shift, "crest.png"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
last := (*events)[len(*events)-1]
|
||||
if strings.Contains(last, "/court/chronicles") || !strings.Contains(last, "/court/notes.md") || !strings.Contains(last, "/court/decrees/crest.png") {
|
||||
t.Fatalf("range selection event %q", last)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user