repos / pico

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

pico / tui / settings
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}