diff --git a/config/development.toml b/config/development.toml index 86d60d1..ca54d00 100644 --- a/config/development.toml +++ b/config/development.toml @@ -57,3 +57,6 @@ interval_s = 1 [tui.tap] interval_s = 60 + +[tui.gamification] +interval_s = 3600 diff --git a/db/migrations/20241125113707_add_gamification_table.sql b/db/migrations/20241125113707_add_gamification_table.sql index 7cef662..46ba33f 100644 --- a/db/migrations/20241125113707_add_gamification_table.sql +++ b/db/migrations/20241125113707_add_gamification_table.sql @@ -4,7 +4,7 @@ CREATE TABLE IF NOT EXISTS gamification ( id INTEGER PRIMARY KEY, name VARCHAR(255) NOT NULL, score INTEGER NOT NULL, - avatar VARCHAR(255) + avatar VARCHAR(255) NOT NULL ); -- +goose StatementEnd diff --git a/db/queries/gamification.sql b/db/queries/gamification.sql index 18e76b6..7081864 100644 --- a/db/queries/gamification.sql +++ b/db/queries/gamification.sql @@ -22,3 +22,8 @@ UPDATE gamification SET score = ? WHERE id = ? RETURNING *; + +-- name: GetAllGamificationByScore :many +SELECT * +FROM gamification +ORDER BY score DESC; diff --git a/go.mod b/go.mod index 96f85c1..20eda78 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,11 @@ module github.com/zeusWPI/scc go 1.23.1 require ( + github.com/charmbracelet/bubbletea v0.25.0 + github.com/charmbracelet/lipgloss v1.0.0 + github.com/disintegration/imaging v1.6.2 github.com/joho/godotenv v1.5.1 + github.com/lucasb-eyer/go-colorful v1.2.0 github.com/spf13/viper v1.19.0 ) @@ -11,8 +15,6 @@ require ( github.com/andybalholm/brotli v1.0.5 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/charmbracelet/bubbles v0.18.0 // indirect - github.com/charmbracelet/bubbletea v0.25.0 // indirect - github.com/charmbracelet/lipgloss v1.0.0 // indirect github.com/charmbracelet/x/ansi v0.4.2 // indirect github.com/containerd/console v1.0.4 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect @@ -22,7 +24,6 @@ require ( github.com/klauspost/compress v1.17.2 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/lrstanley/bubblezone v0.0.0-20240125042004-b7bafc493195 // indirect - github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-localereader v0.0.1 // indirect @@ -36,6 +37,7 @@ require ( github.com/valyala/fasthttp v1.51.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect golang.org/x/crypto v0.27.0 // indirect + golang.org/x/image v0.11.0 // indirect golang.org/x/net v0.23.0 // indirect golang.org/x/sync v0.8.0 // indirect golang.org/x/term v0.24.0 // indirect diff --git a/go.sum b/go.sum index a8270f5..9a589d4 100644 --- a/go.sum +++ b/go.sum @@ -4,22 +4,26 @@ github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/ github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= +github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0= github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw= github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM= github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg= -github.com/charmbracelet/lipgloss v0.10.0 h1:KWeXFSexGcfahHX+54URiZGkBFazf70JNMtwg/AFW3s= -github.com/charmbracelet/lipgloss v0.10.0/go.mod h1:Wig9DSfvANsxqkRsqj6x87irdy123SR4dOXlKa91ciE= github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg= github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo= github.com/charmbracelet/x/ansi v0.4.2 h1:0JM6Aj/g/KC154/gOP4vfxun0ff6itogDYk41kof+qk= github.com/charmbracelet/x/ansi v0.4.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a h1:G99klV19u0QnhiizODirwVksQB91TJKV/UaTnACcG30= +github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro= github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= +github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= @@ -92,8 +96,6 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.3 h1:utMvzDsuh3suAEnhH0RdHmoPbU648o6CvXxTx4SBMOw= -github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= @@ -129,29 +131,63 @@ github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1S github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= +golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.11.0 h1:ds2RoQvBvYTiJkwpSFDwCcDFNX7DqjL2WsUgTNk0Ooo= +golang.org/x/image v0.11.0/go.mod h1:bglhjqbqVuEb9e9+eNR45Jfu7D+T4Qan+NhQk8Ck2P8= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/internal/cmd/tui.go b/internal/cmd/tui.go index 850af6f..dccdedf 100644 --- a/internal/cmd/tui.go +++ b/internal/cmd/tui.go @@ -60,13 +60,23 @@ func tuiPeriodicUpdates(db *db.DB, p *tea.Program, updateData view.UpdateData, d ticker := time.NewTicker(time.Duration(updateData.Interval) * time.Second) defer ticker.Stop() + // Immediatly update once + msg, err := updateData.Update(db, updateData.View) + if err != nil { + zap.S().Error("TUI: Error updating ", updateData.Name, "\n", err) + } + + if msg != nil { + p.Send(msg) + } + for { select { case <-done: zap.S().Info("TUI: Stopping periodic update for ", updateData.Name) return case <-ticker.C: - // Update tap + // Update msg, err := updateData.Update(db, updateData.View) if err != nil { zap.S().Error("TUI: Error updating ", updateData.Name, "\n", err) diff --git a/internal/pkg/db/dto/gamification.go b/internal/pkg/db/dto/gamification.go index 8aa6484..e00a6e4 100644 --- a/internal/pkg/db/dto/gamification.go +++ b/internal/pkg/db/dto/gamification.go @@ -20,6 +20,11 @@ func GamificationDTO(gam sqlc.Gamification) *Gamification { } } +// Equal compares 2 Gamification objects for equality +func (g *Gamification) Equal(g2 Gamification) bool { + return g.Name == g2.Name && g.Score == g2.Score && g.Avatar == g2.Avatar +} + // CreateParams converts a Gamification DTO to a sqlc CreateGamificationParams object func (g *Gamification) CreateParams() sqlc.CreateGamificationParams { return sqlc.CreateGamificationParams{ diff --git a/internal/pkg/db/sqlc/gamification.sql.go b/internal/pkg/db/sqlc/gamification.sql.go index c83ef13..7da2289 100644 --- a/internal/pkg/db/sqlc/gamification.sql.go +++ b/internal/pkg/db/sqlc/gamification.sql.go @@ -81,6 +81,40 @@ func (q *Queries) GetAllGamification(ctx context.Context) ([]Gamification, error return items, nil } +const getAllGamificationByScore = `-- name: GetAllGamificationByScore :many +SELECT id, name, score, avatar +FROM gamification +ORDER BY score DESC +` + +func (q *Queries) GetAllGamificationByScore(ctx context.Context) ([]Gamification, error) { + rows, err := q.db.QueryContext(ctx, getAllGamificationByScore) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Gamification + for rows.Next() { + var i Gamification + if err := rows.Scan( + &i.ID, + &i.Name, + &i.Score, + &i.Avatar, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const updateGamificationScore = `-- name: UpdateGamificationScore :one diff --git a/ui/view/gamification.go b/ui/view/gamification.go new file mode 100644 index 0000000..7608f1d --- /dev/null +++ b/ui/view/gamification.go @@ -0,0 +1,183 @@ +package view + +import ( + "context" + "database/sql" + "fmt" + "image" + "os" + "path/filepath" + "strconv" + "strings" + + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/disintegration/imaging" + "github.com/lucasb-eyer/go-colorful" + "github.com/zeusWPI/scc/internal/pkg/db" + "github.com/zeusWPI/scc/internal/pkg/db/dto" + "github.com/zeusWPI/scc/pkg/config" +) + +var width = 20 + +var ( + base = lipgloss.NewStyle() + columnStyle = base.MarginLeft(1) + nameBase = base.BorderStyle(lipgloss.NormalBorder()).BorderBottom(true).BorderForeground(lipgloss.Color("#383838")).Width(width).Align(lipgloss.Center) + nameStyles = []lipgloss.Style{ + nameBase.Foreground(lipgloss.Color("#FFD700")), + nameBase.Foreground(lipgloss.Color("#FF7F00")), + nameBase.Foreground(lipgloss.Color("#CD7F32")), + nameBase, + } + scoreStyle = base.Width(width).Align(lipgloss.Center) +) + +// GamificationModel represents the view model for gamification +type GamificationModel struct { + db *db.DB + leaderboard []gamificationItem +} + +type gamificationItem struct { + image image.Image + item dto.Gamification +} + +// GamificationMsg contains the data to update the gamification model +type GamificationMsg struct { + leaderboard []gamificationItem +} + +// NewGamificationModel initializes a new gamification model +func NewGamificationModel(db *db.DB) View { + return &GamificationModel{db: db, leaderboard: []gamificationItem{}} +} + +// Init starts the gamification view +func (g *GamificationModel) Init() tea.Cmd { + return nil +} + +// Update updates the gamification view +func (g *GamificationModel) Update(msg tea.Msg) (View, tea.Cmd) { + switch msg := msg.(type) { + case GamificationMsg: + g.leaderboard = msg.leaderboard + } + + return g, nil +} + +// View draws the gamification view +func (g *GamificationModel) View() string { + columns := make([]string, 0, len(g.leaderboard)) + + for i, item := range g.leaderboard { + user := lipgloss.JoinVertical(lipgloss.Left, + nameStyles[i%len(nameStyles)].Render(fmt.Sprintf("%d. %s", i+1, item.item.Name)), + scoreStyle.Render(strconv.Itoa(int(item.item.Score))), + ) + + column := lipgloss.JoinVertical(lipgloss.Left, gamificationToString(width, item.image), user) + columns = append(columns, columnStyle.Render(column)) + } + + list := lipgloss.JoinHorizontal(lipgloss.Top, columns...) + + return list +} + +// GetUpdateDatas get all update functions for the gamification view +func (g *GamificationModel) GetUpdateDatas() []UpdateData { + return []UpdateData{ + { + Name: "gamification leaderboard", + View: g, + Update: updateLeaderboard, + Interval: config.GetDefaultInt("tui.gamification.interval_s", 3600), + }, + } +} + +func updateLeaderboard(db *db.DB, view View) (tea.Msg, error) { + g := view.(*GamificationModel) + + gams, err := db.Queries.GetAllGamificationByScore(context.Background()) + if err != nil { + if err == sql.ErrNoRows { + err = nil + } + return nil, err + } + + // Check if both leaderboards are equal + equal := false + if len(g.leaderboard) == len(gams) { + equal = true + for i, l := range g.leaderboard { + if !l.item.Equal(*dto.GamificationDTO(gams[i])) { + equal = false + break + } + } + } + + if equal { + return nil, nil + } + + msg := GamificationMsg{leaderboard: []gamificationItem{}} + for _, gam := range gams { + if gam.Avatar == "" { + // No avatar downloaded + msg.leaderboard = append(msg.leaderboard, gamificationItem{image: nil, item: *dto.GamificationDTO(gam)}) + continue + } + + file, err := os.Open(filepath.Clean(gam.Avatar)) + if err != nil { + return nil, err + } + defer func() { + _ = file.Close() + }() + + img, _, err := image.Decode(file) + if err != nil { + return nil, err + } + + msg.leaderboard = append(msg.leaderboard, gamificationItem{image: img, item: *dto.GamificationDTO(gam)}) + } + + return msg, nil +} + +func gamificationToString(width int, img image.Image) string { + img = imaging.Resize(img, width, 0, imaging.Lanczos) + b := img.Bounds() + imageWidth := b.Max.X + h := b.Max.Y + str := strings.Builder{} + + for heightCounter := 0; heightCounter < h; heightCounter += 2 { + for x := imageWidth; x < width; x += 2 { + str.WriteString(" ") + } + + for x := 0; x < imageWidth; x++ { + c1, _ := colorful.MakeColor(img.At(x, heightCounter)) + color1 := lipgloss.Color(c1.Hex()) + c2, _ := colorful.MakeColor(img.At(x, heightCounter+1)) + color2 := lipgloss.Color(c2.Hex()) + str.WriteString(lipgloss.NewStyle().Foreground(color1). + Background(color2).Render("▀")) + } + + str.WriteString("\n") + } + + return str.String() +}