Eric Bower
·
15 Dec 24
settings.go
1package settings
2
3import (
4 "fmt"
5 "time"
6
7 tea "github.com/charmbracelet/bubbletea"
8 "github.com/charmbracelet/lipgloss"
9 "github.com/charmbracelet/lipgloss/table"
10 "github.com/picosh/pico/db"
11 "github.com/picosh/pico/tui/common"
12 "github.com/picosh/pico/tui/pages"
13 "github.com/picosh/utils"
14)
15
16var maxWidth = 50
17
18type state int
19
20const (
21 stateLoading state = iota
22 stateReady
23)
24
25type focus int
26
27const (
28 focusNone = iota
29 focusAnalytics
30)
31
32type featuresLoadedMsg []*db.FeatureFlag
33
34type Model struct {
35 shared *common.SharedModel
36 features []*db.FeatureFlag
37 state state
38 focus focus
39}
40
41func NewModel(shrd *common.SharedModel) Model {
42 return Model{
43 shared: shrd,
44 state: stateLoading,
45 focus: focusNone,
46 }
47}
48
49func (m Model) Init() tea.Cmd {
50 return m.fetchFeatures()
51}
52
53func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
54 switch msg := msg.(type) {
55 case tea.KeyMsg:
56 switch msg.String() {
57 case "q", "esc":
58 return m, pages.Navigate(pages.MenuPage)
59 case "tab":
60 if m.focus == focusNone {
61 m.focus = focusAnalytics
62 } else {
63 m.focus = focusNone
64 }
65 case "enter":
66 if m.focus == focusAnalytics {
67 return m, m.toggleAnalytics()
68 }
69 }
70
71 case featuresLoadedMsg:
72 m.state = stateReady
73 m.focus = focusNone
74 m.features = msg
75 }
76 return m, nil
77}
78
79func (m Model) View() string {
80 if m.state == stateLoading {
81 return "Loading ..."
82 }
83 return m.servicesView() + "\n" + m.featuresView() + "\n" + m.analyticsView()
84}
85
86func (m Model) findAnalyticsFeature() *db.FeatureFlag {
87 for _, feature := range m.features {
88 if feature.Name == "analytics" {
89 return feature
90 }
91 }
92 return nil
93}
94
95func (m Model) analyticsView() string {
96 banner := `Get usage statistics on your blog, blog posts, and pages sites. For example, see unique visitors, most popular URLs, and top referers.
97
98We do not collect usage statistic unless analytics is enabled. Further, when analytics are disabled we do not purge usage statistics.
99
100We will only store usage statistics for 1 year from when the event was created.`
101
102 str := ""
103 hasPlus := m.shared.PlusFeatureFlag != nil
104 if hasPlus {
105 ff := m.findAnalyticsFeature()
106 hasFocus := m.focus == focusAnalytics
107 if ff == nil {
108 str += banner + "\n\nEnable analytics " + common.OKButtonView(m.shared.Styles, hasFocus, false)
109 } else {
110 str += "Disable analytics " + common.OKButtonView(m.shared.Styles, hasFocus, false)
111 }
112 } else {
113 str += banner + "\n\n" + m.shared.Styles.Error.SetString("Analytics is only available to pico+ users.").String()
114 }
115
116 return m.shared.Styles.RoundedBorder.Width(maxWidth).SetString(str).String()
117}
118
119func (m Model) servicesView() string {
120 headers := []string{
121 "Name",
122 "Status",
123 }
124
125 hasPlus := m.shared.PlusFeatureFlag != nil
126
127 data := [][]string{
128 {"prose", "active"},
129 {"pipe", "active"},
130 {"pastes", "active"},
131 {"rss-to-email", "active"},
132 }
133
134 if hasPlus {
135 data = append(
136 data,
137 []string{"pages", "active"},
138 []string{"tuns", "active"},
139 []string{"irc bouncer", "active"},
140 )
141 } else {
142 data = append(
143 data,
144 []string{"pages", "free tier"},
145 []string{"tuns", "requires pico+"},
146 []string{"IRC bouncer", "requires pico+"},
147 )
148 }
149
150 t := table.New().
151 Border(lipgloss.RoundedBorder()).
152 BorderStyle(m.shared.Styles.Renderer.NewStyle().BorderForeground(common.Indigo)).
153 Width(maxWidth).
154 Headers(headers...).
155 Rows(data...)
156 return "Services\n" + t.String()
157}
158
159func (m Model) featuresView() string {
160 headers := []string{
161 "Name",
162 "Quota (GB)",
163 "Expires At",
164 }
165
166 data := [][]string{}
167 for _, feature := range m.features {
168 storeMax := utils.BytesToGB(int(feature.FindStorageMax(0)))
169 row := []string{
170 feature.Name,
171 fmt.Sprintf("%.2f", storeMax),
172 feature.ExpiresAt.Format(common.DateFormat),
173 }
174 data = append(data, row)
175 }
176 t := table.New().
177 Border(lipgloss.RoundedBorder()).
178 BorderStyle(m.shared.Styles.Renderer.NewStyle().BorderForeground(common.Indigo)).
179 Width(maxWidth).
180 Headers(headers...).
181 Rows(data...)
182 return "Features\n" + t.String()
183}
184
185func (m Model) fetchFeatures() tea.Cmd {
186 return func() tea.Msg {
187 features, err := m.shared.Dbpool.FindFeaturesForUser(m.shared.User.ID)
188 if err != nil {
189 return common.ErrMsg{Err: err}
190 }
191 return featuresLoadedMsg(features)
192 }
193}
194
195func (m Model) toggleAnalytics() tea.Cmd {
196 return func() tea.Msg {
197 if m.findAnalyticsFeature() == nil {
198 now := time.Now()
199 expiresAt := now.AddDate(100, 0, 0)
200 _, err := m.shared.Dbpool.InsertFeature(m.shared.User.ID, "analytics", expiresAt)
201 if err != nil {
202 return common.ErrMsg{Err: err}
203 }
204 } else {
205 err := m.shared.Dbpool.RemoveFeature(m.shared.User.ID, "analytics")
206 if err != nil {
207 return common.ErrMsg{Err: err}
208 }
209 }
210
211 cmd := m.fetchFeatures()
212 return cmd()
213 }
214}