repos / pico

pico services - prose.sh, pastes.sh, imgs.sh, feeds.sh, pgs.sh
git clone https://github.com/picosh/pico.git

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
A tui/common/err.go
+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() }
M tui/menu/menu.go
+2, -0
 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 }
M tui/pages/pages.go
+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 ""
M tui/pubkeys/keys.go
+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 	}
A tui/settings/settings.go
+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+}
M tui/ui.go
+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: