- commit
- 16c06a6
- parent
- 7f4090c
- author
- Eric Bower
- date
- 2024-05-17 21:02:03 +0000 UTC
feat(tui): settings page
6 files changed,
+200,
-14
+7,
-0
1@@ -0,0 +1,7 @@
2+package common
3+
4+type ErrMsg struct {
5+ Err error
6+}
7+
8+func (e ErrMsg) Error() string { return e.Err.Error() }
1@@ -18,6 +18,7 @@ const (
2 TokensChoice
3 NotificationsChoice
4 PlusChoice
5+ SettingsChoice
6 ChatChoice
7 ExitChoice
8 UnsetChoice // set when no choice has been made
9@@ -29,6 +30,7 @@ var menuChoices = map[menuChoice]string{
10 TokensChoice: "Manage tokens",
11 NotificationsChoice: "Notifications",
12 PlusChoice: "Pico+",
13+ SettingsChoice: "Settings",
14 ChatChoice: "Chat",
15 ExitChoice: "Exit",
16 }
+3,
-0
1@@ -13,6 +13,7 @@ const (
2 TokensPage
3 NotificationsPage
4 PlusPage
5+ SettingsPage
6 )
7
8 type NavigateMsg struct{ Page }
9@@ -41,6 +42,8 @@ func ToTitle(page Page) string {
10 return "api tokens"
11 case PubkeysPage:
12 return "pubkeys"
13+ case SettingsPage:
14+ return "settings"
15 }
16
17 return ""
+5,
-11
1@@ -29,12 +29,6 @@ const (
2 keyDeleting
3 )
4
5-type errMsg struct {
6- err error
7-}
8-
9-func (e errMsg) Error() string { return e.err.Error() }
10-
11 type (
12 keysLoadedMsg []*db.PublicKey
13 unlinkedKeyMsg int
14@@ -158,8 +152,8 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
15 }
16 }
17
18- case errMsg:
19- m.err = msg.err
20+ case common.ErrMsg:
21+ m.err = msg.Err
22 return m, nil
23
24 case keysLoadedMsg:
25@@ -312,7 +306,7 @@ func FetchKeys(shrd common.SharedModel) tea.Cmd {
26 return func() tea.Msg {
27 ak, err := shrd.Dbpool.FindKeysForUser(shrd.User)
28 if err != nil {
29- return errMsg{err}
30+ return common.ErrMsg{Err: err}
31 }
32 return keysLoadedMsg(ak)
33 }
34@@ -324,7 +318,7 @@ func (m *Model) unlinkKey() tea.Cmd {
35 id := m.keys[m.getSelectedIndex()].ID
36 err := m.shared.Dbpool.RemoveKeys([]string{id})
37 if err != nil {
38- return errMsg{err}
39+ return common.ErrMsg{Err: err}
40 }
41 return unlinkedKeyMsg(m.index)
42 }
43@@ -336,7 +330,7 @@ func (m *Model) deleteAccount() tea.Cmd {
44 m.shared.Logger.Info("user requested account deletion", "user", m.shared.User.Name, "id", id)
45 err := m.shared.Dbpool.RemoveUsers([]string{id})
46 if err != nil {
47- return errMsg{err}
48+ return common.ErrMsg{Err: err}
49 }
50 return unlinkedKeyMsg(m.index)
51 }
+176,
-0
1@@ -0,0 +1,176 @@
2+package settings
3+
4+import (
5+ "fmt"
6+ "time"
7+
8+ tea "github.com/charmbracelet/bubbletea"
9+ "github.com/charmbracelet/lipgloss"
10+ "github.com/charmbracelet/lipgloss/table"
11+ "github.com/picosh/pico/db"
12+ "github.com/picosh/pico/shared"
13+ "github.com/picosh/pico/tui/common"
14+ "github.com/picosh/pico/tui/pages"
15+)
16+
17+type state int
18+
19+const (
20+ stateLoading state = iota
21+ stateReady
22+)
23+
24+type focus int
25+
26+const (
27+ focusNone = iota
28+ focusAnalytics
29+)
30+
31+type featuresLoadedMsg []*db.FeatureFlag
32+
33+type Model struct {
34+ shared common.SharedModel
35+ features []*db.FeatureFlag
36+ state state
37+ focus focus
38+}
39+
40+func NewModel(shrd common.SharedModel) Model {
41+ return Model{
42+ shared: shrd,
43+ state: stateLoading,
44+ focus: focusNone,
45+ }
46+}
47+
48+func (m Model) Init() tea.Cmd {
49+ return m.fetchFeatures()
50+}
51+
52+func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
53+ switch msg := msg.(type) {
54+ case tea.KeyMsg:
55+ switch msg.String() {
56+ case "q", "esc":
57+ return m, pages.Navigate(pages.MenuPage)
58+ case "tab":
59+ if m.focus == focusNone {
60+ m.focus = focusAnalytics
61+ } else {
62+ m.focus = focusNone
63+ }
64+ case "enter":
65+ if m.focus == focusAnalytics {
66+ return m, m.toggleAnalytics()
67+ }
68+ }
69+
70+ case featuresLoadedMsg:
71+ m.state = stateReady
72+ m.focus = focusNone
73+ m.features = msg
74+ }
75+ return m, nil
76+}
77+
78+func (m Model) View() string {
79+ if m.state == stateLoading {
80+ return "Loading ..."
81+ }
82+ return m.featuresView() + "\n" + m.analyticsView()
83+}
84+
85+func (m Model) findAnalyticsFeature() *db.FeatureFlag {
86+ for _, feature := range m.features {
87+ if feature.Name == "analytics" {
88+ return feature
89+ }
90+ }
91+ return nil
92+}
93+
94+func (m Model) analyticsView() string {
95+ banner := `Get usage statistics on your blog, blog posts, and
96+pages sites. For example, see unique visitors, most popular URLs,
97+and top referers.
98+
99+We do not collect usage statistic unless analytics is enabled.
100+Further, when analytics are disabled we do not purge usage statistics.
101+
102+We will only store usage statistics for 1 year from when the event
103+was created.`
104+
105+ str := ""
106+ hasPlus := m.shared.PlusFeatureFlag != nil
107+ if hasPlus {
108+ ff := m.findAnalyticsFeature()
109+ hasFocus := m.focus == focusAnalytics
110+ if ff == nil {
111+ str += banner + "\n\nEnable analytics " + common.OKButtonView(m.shared.Styles, hasFocus, false)
112+ } else {
113+ str += "Disable analytics " + common.OKButtonView(m.shared.Styles, hasFocus, false)
114+ }
115+ } else {
116+ str += banner + "\n\n" + m.shared.Styles.Error.SetString("Analytics is only available to pico+ users.").String()
117+ }
118+
119+ return m.shared.Styles.RoundedBorder.SetString(str).String()
120+}
121+
122+func (m Model) featuresView() string {
123+ headers := []string{
124+ "Name",
125+ "Quota (GB)",
126+ "Expires At",
127+ }
128+
129+ data := [][]string{}
130+ for _, feature := range m.features {
131+ storeMax := shared.BytesToGB(int(feature.FindStorageMax(0)))
132+ row := []string{
133+ feature.Name,
134+ fmt.Sprintf("%.2f", storeMax),
135+ feature.ExpiresAt.Format("2006-01-02"),
136+ }
137+ data = append(data, row)
138+ }
139+ t := table.New().
140+ Border(lipgloss.RoundedBorder()).
141+ BorderStyle(m.shared.Styles.Renderer.NewStyle().BorderForeground(common.Indigo)).
142+ Width(m.shared.Width).
143+ Headers(headers...).
144+ Rows(data...)
145+ return "Features\n" + t.String()
146+}
147+
148+func (m Model) fetchFeatures() tea.Cmd {
149+ return func() tea.Msg {
150+ features, err := m.shared.Dbpool.FindFeaturesForUser(m.shared.User.ID)
151+ if err != nil {
152+ return common.ErrMsg{Err: err}
153+ }
154+ return featuresLoadedMsg(features)
155+ }
156+}
157+
158+func (m Model) toggleAnalytics() tea.Cmd {
159+ return func() tea.Msg {
160+ if m.findAnalyticsFeature() == nil {
161+ now := time.Now()
162+ expiresAt := now.AddDate(100, 0, 0)
163+ _, err := m.shared.Dbpool.InsertFeature(m.shared.User.ID, "analytics", expiresAt)
164+ if err != nil {
165+ return common.ErrMsg{Err: err}
166+ }
167+ } else {
168+ err := m.shared.Dbpool.RemoveFeature(m.shared.User.ID, "analytics")
169+ if err != nil {
170+ return common.ErrMsg{Err: err}
171+ }
172+ }
173+
174+ cmd := m.fetchFeatures()
175+ return cmd()
176+ }
177+}
+7,
-3
1@@ -17,6 +17,7 @@ import (
2 "github.com/picosh/pico/tui/pages"
3 "github.com/picosh/pico/tui/plus"
4 "github.com/picosh/pico/tui/pubkeys"
5+ "github.com/picosh/pico/tui/settings"
6 "github.com/picosh/pico/tui/tokens"
7 )
8
9@@ -40,7 +41,7 @@ func NewUI(shared common.SharedModel) *UI {
10 m := &UI{
11 shared: shared,
12 state: initState,
13- pages: make([]tea.Model, 8),
14+ pages: make([]tea.Model, 9),
15 }
16 return m
17 }
18@@ -74,6 +75,7 @@ func (m *UI) Init() tea.Cmd {
19 m.pages[pages.TokensPage] = tokens.NewModel(m.shared)
20 m.pages[pages.NotificationsPage] = notifications.NewModel(m.shared)
21 m.pages[pages.PlusPage] = plus.NewModel(m.shared)
22+ m.pages[pages.SettingsPage] = settings.NewModel(m.shared)
23 if m.shared.User == nil {
24 m.activePage = pages.CreateAccountPage
25 } else {
26@@ -129,10 +131,12 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
27 m.activePage = pages.PubkeysPage
28 case menu.TokensChoice:
29 m.activePage = pages.TokensPage
30- case menu.PlusChoice:
31- m.activePage = pages.PlusPage
32 case menu.NotificationsChoice:
33 m.activePage = pages.NotificationsPage
34+ case menu.PlusChoice:
35+ m.activePage = pages.PlusPage
36+ case menu.SettingsChoice:
37+ m.activePage = pages.SettingsPage
38 case menu.ChatChoice:
39 return m, LoadChat(m.shared)
40 case menu.ExitChoice: