Add account components 386-405; Reduce setting, PinInput Cmd+V
Twenty account components: SettingsList, OptionCycler, KeyBindings, ControllerMap, ProfileCard, CharacterCreator, LoginForm, PasswordGrid, SessionList, LanguagePicker, PalettePicker, VolumeMixer, DifficultySelect, AccessibilityOptions, AccountSwitcher, PlayerSelect, TwoFactor, About, PatchNotes and FeedbackForm. The components never check credentials, and no secret reaches the log: LoginForm reports Submitted and the caller counts Failures; each new failure clears the password, refocuses it, shakes the form and names no field. TwoFactor builds on input.PinInput and drains an epoch-aligned period bar. PalettePicker draws each tile in its own palette on a pixel.Pad grid. pixel.Settings.Reduce stills motion like the system preference, the micro font gains '?', display.Face draws avatar faces anywhere, and input.PinInput pastes on Cmd+V as native hosts send it.
This commit is contained in:
38 files changed
+2788
-13
No files matched your search
@@ -0,0 +1,167 @@
|
||||
package gallery
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/account"
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/icon"
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/input"
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
||||
)
|
||||
|
||||
var accountEntries = []Entry{
|
||||
{386, "SettingsList", "设置列表", "account", "Labels on the left, controls on the right, sections on top. Tidy.", demoSettingsList},
|
||||
{387, "OptionCycler", "选项切换", "account", "◀ and ▶ walk the choices; past the end it comes around.", demoOptionCycler},
|
||||
{388, "KeyBindings", "按键绑定", "account", "Enter, then press the new key. Two actions on one key glow red.", demoKeyBindings},
|
||||
{389, "ControllerMap", "手柄图", "account", "Which button does what, with lines to prove it.", demoControllerMap},
|
||||
{390, "ProfileCard", "玩家卡", "account", "Face, name, level, title and the numbers that matter.", demoProfileCard},
|
||||
{391, "CharacterCreator", "角色创建", "account", "Cycle hair, eyes, outfit and colors; the hero changes as you do.", demoCharacterCreator},
|
||||
{392, "LoginForm", "登录表单", "account", "Try player1 and hunter2. Anything else, and the form shakes its head.", demoLoginForm},
|
||||
{393, "PasswordGrid", "密码网格", "account", "Spell a password from the grid, as cartridges once asked. Try JUSTINBAILEY.", demoPasswordGrid},
|
||||
{394, "SessionList", "会话设备", "account", "Every device still signed in, and a way to kick each out.", demoSessionList},
|
||||
{395, "LanguagePicker", "语言选择", "account", "Languages by their own names, flags beside. ▶ marks yours.", demoLanguagePicker},
|
||||
}
|
||||
|
||||
type accountDemo struct {
|
||||
music, sfx, fullscreen, scanlines bool
|
||||
speed, lang, failures int
|
||||
binds []account.Binding
|
||||
look account.Look
|
||||
name, password, grid string
|
||||
sessions []account.Session
|
||||
}
|
||||
|
||||
func accountState(c *ui.Context) *accountDemo {
|
||||
now := pixel.K(c).Now()
|
||||
return ui.Local(c.Root(), demoKey("account"), func() accountDemo {
|
||||
return accountDemo{
|
||||
music: true, sfx: true, scanlines: true, speed: 1,
|
||||
binds: []account.Binding{
|
||||
{Action: "Jump", Key: ui.KeySpace}, {Action: "Attack", Key: ui.KeyZ}, {Action: "Dash", Key: ui.KeyZ}, {Action: "Map", Key: ui.KeyM},
|
||||
},
|
||||
sessions: []account.Session{
|
||||
{Device: "This laptop", Icon: icon.Globe, Seen: now, Current: true},
|
||||
{Device: "Game console", Icon: icon.Gamepad, Seen: now.Add(-2 * time.Hour)},
|
||||
{Device: "Old phone", Icon: icon.Phone, Seen: now.Add(-72 * time.Hour)},
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
var speeds = []string{"SLOW", "NORMAL", "FAST", "INSTANT"}
|
||||
|
||||
func demoSettingsList(c *ui.Context, log func(string)) {
|
||||
s := accountState(c)
|
||||
toggle := func(label string, on *bool) func() {
|
||||
return func() {
|
||||
// The row shows the label; the switch says it aloud.
|
||||
if input.Switch(c, "", on, input.SwitchOptions{Label: label}).Changed {
|
||||
log(fmt.Sprintf("%s %v", label, *on))
|
||||
}
|
||||
}
|
||||
}
|
||||
account.SettingsList(c, "Settings", []account.SettingsSection{
|
||||
{Title: "AUDIO", Settings: []account.Setting{{Label: "Music", Control: toggle("Music", &s.music)}, {Label: "Sound effects", Control: toggle("Sound effects", &s.sfx)}}},
|
||||
{Title: "VIDEO", Settings: []account.Setting{{Label: "Full screen", Control: toggle("Full screen", &s.fullscreen)}, {Label: "Scanlines", Control: toggle("Scanlines", &s.scanlines)}}},
|
||||
{Title: "GAME", Settings: []account.Setting{{Label: "Text speed", Control: func() {
|
||||
if account.OptionCycler(c, "Text speed", speeds, &s.speed, account.OptionCyclerOptions{}).Changed {
|
||||
log("text speed " + speeds[s.speed])
|
||||
}
|
||||
}}}},
|
||||
}, account.SettingsListOptions{})
|
||||
}
|
||||
|
||||
func demoOptionCycler(c *ui.Context, log func(string)) {
|
||||
s := accountState(c)
|
||||
if account.OptionCycler(c, "Text speed", speeds, &s.speed, account.OptionCyclerOptions{}).Changed {
|
||||
log("text speed " + speeds[s.speed])
|
||||
}
|
||||
}
|
||||
|
||||
func demoKeyBindings(c *ui.Context, log func(string)) {
|
||||
s := accountState(c)
|
||||
if account.KeyBindings(c, "Controls", s.binds, account.KeyBindingsOptions{}).Changed {
|
||||
log("rebound")
|
||||
}
|
||||
}
|
||||
|
||||
func demoControllerMap(c *ui.Context, log func(string)) {
|
||||
account.ControllerMap(c, "Pad", map[account.PadButton]string{
|
||||
account.DPad: "MOVE", account.ButtonA: "JUMP", account.ButtonB: "RUN", account.ButtonX: "MAP", account.ButtonY: "ITEM",
|
||||
account.ShoulderL: "PREV", account.ShoulderR: "NEXT", account.Start: "PAUSE", account.Select: "MENU",
|
||||
}, account.ControllerMapOptions{})
|
||||
}
|
||||
|
||||
func demoProfileCard(c *ui.Context, log func(string)) {
|
||||
account.ProfileCard(c, account.Profile{Name: "Aria", Title: "Dragon Slayer", Level: 12, Stats: []account.Stat{
|
||||
{Name: "HP", Value: 30, Max: 40}, {Name: "MP", Value: 12, Max: 20}, {Name: "EXP", Value: 640, Max: 1000},
|
||||
}}, account.ProfileCardOptions{})
|
||||
}
|
||||
|
||||
func demoCharacterCreator(c *ui.Context, log func(string)) {
|
||||
s := accountState(c)
|
||||
if account.CharacterCreator(c, "New hero", &s.look, account.CharacterCreatorOptions{}).Changed {
|
||||
log(fmt.Sprintf("look %+v", s.look))
|
||||
}
|
||||
}
|
||||
|
||||
func demoLoginForm(c *ui.Context, log func(string)) {
|
||||
s := accountState(c)
|
||||
if account.LoginForm(c, "WELCOME BACK", &s.name, &s.password, account.LoginFormOptions{Failures: s.failures}).Submitted {
|
||||
// The demo checks; a real game asks its server.
|
||||
if s.name == "player1" && s.password == "hunter2" {
|
||||
log("logged in")
|
||||
} else {
|
||||
s.failures++
|
||||
log("wrong name or password")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func demoPasswordGrid(c *ui.Context, log func(string)) {
|
||||
s := accountState(c)
|
||||
r := account.PasswordGrid(c, "Password", &s.grid, account.PasswordGridOptions{})
|
||||
if r.Changed {
|
||||
log(fmt.Sprintf("%d of 12", len(s.grid)))
|
||||
}
|
||||
if r.Submitted {
|
||||
if s.grid == "JUSTINBAILEY" {
|
||||
log("suit off. you know the one.")
|
||||
} else {
|
||||
log("wrong password")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func demoSessionList(c *ui.Context, log func(string)) {
|
||||
s := accountState(c)
|
||||
if r := account.SessionList(c, "Signed in", s.sessions, account.SessionListOptions{}); r.SignedOut >= 0 {
|
||||
log("signed out " + s.sessions[r.SignedOut].Device)
|
||||
s.sessions = append(s.sessions[:r.SignedOut:r.SignedOut], s.sessions[r.SignedOut+1:]...)
|
||||
}
|
||||
}
|
||||
|
||||
var languages = []account.Language{
|
||||
{Name: "English", Flag: flag("llcccrrcccll cllccrrccllc ccllcrrcllcc rrrrrrrrrrrr rrrrrrrrrrrr ccllcrrcllcc cllccrrccllc llcccrrcccll")},
|
||||
{Name: "中文", Flag: flag("rryrrrrrrrrr ryyyrrrrrrrr rryrryrrrrrr rrrrrrrrrrrr rrryrrrrrrrr rrrrrrrrrrrr rrrrrrrrrrrr rrrrrrrrrrrr")},
|
||||
{Name: "日本語", Flag: flag("llllllllllll llllllllllll lllllrrlllll llllrrrrllll llllrrrrllll lllllrrlllll llllllllllll llllllllllll")},
|
||||
{Name: "Français", Flag: flag("ccccllllrrrr ccccllllrrrr ccccllllrrrr ccccllllrrrr ccccllllrrrr ccccllllrrrr ccccllllrrrr ccccllllrrrr")},
|
||||
{Name: "Deutsch", Flag: flag("kkkkkkkkkkkk kkkkkkkkkkkk kkkkkkkkkkkk rrrrrrrrrrrr rrrrrrrrrrrr rrrrrrrrrrrr yyyyyyyyyyyy yyyyyyyyyyyy")},
|
||||
{Name: "Español", Flag: flag("rrrrrrrrrrrr rrrrrrrrrrrr yyyyyyyyyyyy yyyyyyyyyyyy yyyyyyyyyyyy yyyyyyyyyyyy rrrrrrrrrrrr rrrrrrrrrrrr")},
|
||||
}
|
||||
|
||||
// flag is string art given as rows split by spaces.
|
||||
func flag(rows string) *pixel.Sprite {
|
||||
return pixel.MustSprite(strings.ReplaceAll(rows, " ", "\n"))
|
||||
}
|
||||
|
||||
func demoLanguagePicker(c *ui.Context, log func(string)) {
|
||||
s := accountState(c)
|
||||
if account.LanguagePicker(c, "Language", languages, &s.lang, account.LanguagePickerOptions{Height: 100}).Changed {
|
||||
log("language " + languages[s.lang].Name)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package gallery
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/account"
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/display"
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
||||
)
|
||||
|
||||
var playerEntries = []Entry{
|
||||
{396, "PalettePicker", "调色板选择", "account", "Each palette as a little live scene. Choose one and the sample wears it.", demoPalettePicker},
|
||||
{397, "VolumeMixer", "音量混合", "account", "Music, effects and voice, each on its own fader.", demoVolumeMixer},
|
||||
{398, "DifficultySelect", "难度选择", "account", "From EASY to NIGHTMARE, one skull a step. Choose wisely.", demoDifficultySelect},
|
||||
{399, "AccessibilityOptions", "无障碍", "account", "Reduce motion and the dot size; the sample below obeys at once.", demoAccessibilityOptions},
|
||||
{400, "AccountSwitcher", "账号切换", "account", "Four player slots in arcade colors. Empty ones wait for a friend.", demoAccountSwitcher},
|
||||
{401, "PlayerSelect", "玩家选择", "account", "1 PLAYER or 2 PLAYERS: the oldest question on the title screen.", demoPlayerSelect},
|
||||
{402, "TwoFactor", "两步验证", "account", "Six digits, typed or pasted. The bar is how long the code lives.", demoTwoFactor},
|
||||
{403, "About", "关于", "account", "The logo, the version, and the people to thank.", demoAbout},
|
||||
{404, "PatchNotes", "更新日志", "account", "What came, what was fixed, what changed, version by version.", demoPatchNotes},
|
||||
{405, "FeedbackForm", "反馈表", "account", "Stars and words. SEND waits until you rate.", demoFeedbackForm},
|
||||
}
|
||||
|
||||
type playerDemo struct {
|
||||
palette, difficulty, slot, players, rating int
|
||||
mix account.Mix
|
||||
access account.Access
|
||||
code, words string
|
||||
}
|
||||
|
||||
func playerState(c *ui.Context) *playerDemo {
|
||||
return ui.Local(c.Root(), demoKey("player"), func() playerDemo {
|
||||
return playerDemo{difficulty: 1, players: 1, mix: account.Mix{BGM: 0.8, SE: 0.6, Voice: 0.9}, access: account.Access{Dot: 2}}
|
||||
})
|
||||
}
|
||||
|
||||
func demoPalettePicker(c *ui.Context, log func(string)) {
|
||||
k := pixel.K(c)
|
||||
s := playerState(c)
|
||||
ui.Column(c).Gap(k.D(6)).Children(func() {
|
||||
if account.PalettePicker(c, "Palette", &s.palette, account.PalettePickerOptions{}).Changed {
|
||||
log("palette " + pixel.Palettes[s.palette].Name)
|
||||
}
|
||||
// Choosing applies: the sample draws in the chosen palette.
|
||||
in := pixel.Current(c)
|
||||
in.Palette = pixel.Palettes[s.palette]
|
||||
pixel.Scope(c, in, func() {
|
||||
display.Sprite(c, slime, display.SpriteOptions{FPS: 4, Scale: 3})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func demoVolumeMixer(c *ui.Context, log func(string)) {
|
||||
s := playerState(c)
|
||||
if account.VolumeMixer(c, "Mixer", &s.mix, account.VolumeMixerOptions{}).Changed {
|
||||
log(fmt.Sprintf("mix %.2f %.2f %.2f", s.mix.BGM, s.mix.SE, s.mix.Voice))
|
||||
}
|
||||
}
|
||||
|
||||
func demoDifficultySelect(c *ui.Context, log func(string)) {
|
||||
s := playerState(c)
|
||||
if account.DifficultySelect(c, "Difficulty", &s.difficulty, account.DifficultySelectOptions{}).Changed {
|
||||
log(fmt.Sprintf("difficulty %d", s.difficulty+1))
|
||||
}
|
||||
}
|
||||
|
||||
func demoAccessibilityOptions(c *ui.Context, log func(string)) {
|
||||
k := pixel.K(c)
|
||||
s := playerState(c)
|
||||
ui.Column(c).Gap(k.D(8)).Children(func() {
|
||||
if account.AccessibilityOptions(c, "Access", &s.access, account.AccessibilityOptionsOptions{}).Changed {
|
||||
log(fmt.Sprintf("reduce %v, dot %d", s.access.Reduce, s.access.Dot))
|
||||
}
|
||||
in := pixel.Current(c)
|
||||
in.Dot, in.Reduce = s.access.Dot, s.access.Reduce
|
||||
pixel.Scope(c, in, func() {
|
||||
display.Sprite(c, slime, display.SpriteOptions{FPS: 4, Scale: 2})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func demoAccountSwitcher(c *ui.Context, log func(string)) {
|
||||
s := playerState(c)
|
||||
if account.AccountSwitcher(c, "Players", [4]string{"Aria", "Brom", "", ""}, &s.slot, account.AccountSwitcherOptions{}).Changed {
|
||||
log(fmt.Sprintf("P%d", s.slot+1))
|
||||
}
|
||||
}
|
||||
|
||||
func demoPlayerSelect(c *ui.Context, log func(string)) {
|
||||
s := playerState(c)
|
||||
r := account.PlayerSelect(c, "Players", &s.players, account.PlayerSelectOptions{})
|
||||
if r.Changed {
|
||||
log(fmt.Sprintf("%d players", s.players))
|
||||
}
|
||||
if r.Chosen {
|
||||
log(fmt.Sprintf("start %dP", s.players))
|
||||
}
|
||||
}
|
||||
|
||||
func demoTwoFactor(c *ui.Context, log func(string)) {
|
||||
s := playerState(c)
|
||||
r := account.TwoFactor(c, "Login code", &s.code, account.TwoFactorOptions{})
|
||||
if r.Changed {
|
||||
log(fmt.Sprintf("%d digits", len(s.code)))
|
||||
}
|
||||
if r.Submitted {
|
||||
log("code sent")
|
||||
}
|
||||
}
|
||||
|
||||
func demoAbout(c *ui.Context, log func(string)) {
|
||||
account.About(c, "8BIT", account.AboutOptions{Version: "1.0.0", Credits: []account.Credit{
|
||||
{Role: "Pixels and code", Name: "The 8bit team"}, {Role: "Native UI", Name: "MyGo"}, {Role: "Font", Name: "Ark Pixel"},
|
||||
}})
|
||||
}
|
||||
|
||||
func demoPatchNotes(c *ui.Context, log func(string)) {
|
||||
account.PatchNotes(c, "Patch notes", []account.Patch{
|
||||
{Version: "v1.1.0", Added: []string{"Palette cycling waterfalls"}, Fixed: []string{"The ghost in level 3 now leaves"}, Changed: []string{"Jump height +1 dot"}},
|
||||
{Version: "v1.0.0", Added: []string{"Everything"}},
|
||||
}, account.PatchNotesOptions{})
|
||||
}
|
||||
|
||||
func demoFeedbackForm(c *ui.Context, log func(string)) {
|
||||
s := playerState(c)
|
||||
if account.FeedbackForm(c, "Feedback", &s.rating, &s.words, account.FeedbackFormOptions{}).Sent {
|
||||
log(fmt.Sprintf("sent %d stars", s.rating))
|
||||
s.rating, s.words = 0, ""
|
||||
}
|
||||
}
|
||||
@@ -71,3 +71,38 @@ func TestCodeDemosLogTheirActions(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccountDemosLogTheirActions(t *testing.T) {
|
||||
quiet := map[string]bool{"NAME": true, "PASSWORD": true, "Login code": true, "Controls": true, "4/5": true, "type:player1": true, "type:hunter2": true}
|
||||
for _, tc := range []struct {
|
||||
path string
|
||||
steps []string
|
||||
}{
|
||||
{"/c/386-settingslist", []string{"Music"}}, {"/c/387-optioncycler", []string{"Text speed"}},
|
||||
{"/c/388-keybindings", []string{"Controls", "key:A"}}, {"/c/391-charactercreator", []string{"HAIR"}},
|
||||
{"/c/392-loginform", []string{"NAME", "type:player1", "PASSWORD", "type:hunter2", "key:Enter"}},
|
||||
{"/c/393-passwordgrid", []string{"SYMBOLS"}}, {"/c/394-sessionlist", []string{"SIGN OUT Old phone"}},
|
||||
{"/c/395-languagepicker", []string{"Deutsch"}}, {"/c/396-palettepicker", []string{"Palette"}},
|
||||
{"/c/397-volumemixer", []string{"BGM"}}, {"/c/398-difficultyselect", []string{"Difficulty"}},
|
||||
{"/c/399-accessibilityoptions", []string{"REDUCE MOTION"}}, {"/c/400-accountswitcher", []string{"P2, Brom"}},
|
||||
{"/c/401-playerselect", []string{"Players"}}, {"/c/402-twofactor", []string{"Login code", "type:1"}},
|
||||
{"/c/405-feedbackform", []string{"4/5", "SEND ▶"}},
|
||||
} {
|
||||
a, tt := open(tc.path)
|
||||
slug := tc.path[3:]
|
||||
for _, step := range tc.steps {
|
||||
before := len(a.logs[slug])
|
||||
switch {
|
||||
case strings.HasPrefix(step, "key:"):
|
||||
tt.Key(0, letters[step[4:]])
|
||||
case strings.HasPrefix(step, "type:"):
|
||||
tt.Type(step[5:])
|
||||
default:
|
||||
click(t, tt, step)
|
||||
}
|
||||
if len(a.logs[slug]) <= before && !quiet[step] {
|
||||
t.Errorf("%s: %s logged nothing: %q", slug, step, a.logs[slug])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+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)
|
||||
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)
|
||||
|
||||
func Entries() []Entry { return entries }
|
||||
|
||||
|
||||
Reference in new issue
Block a user