aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBobby <[email protected]>2024-09-08 18:34:58 -0400
committerBobby <[email protected]>2024-09-08 18:34:58 -0400
commite25611bde49fe2db28a006aca3ea49eece046c5f (patch)
treedae68f8784010ff9002fe0e3c5d228879c15a25f
parent61bc5b38044bc52442415f83390ba72ed3b27491 (diff)
downloadyato-e25611bde49fe2db28a006aca3ea49eece046c5f.tar.xz
yato-e25611bde49fe2db28a006aca3ea49eece046c5f.zip
Image Rendering. Recommendations Thingy. Basic HomeHEADmain
-rw-r--r--config/colors.go15
-rw-r--r--config/constants.go4
-rw-r--r--go.mod7
-rw-r--r--go.sum4
-rw-r--r--lib/auth.go (renamed from utils/auth.go)21
-rw-r--r--lib/image_cache.go98
-rw-r--r--lib/image_renderer.go158
-rw-r--r--lib/recommendations.go79
-rw-r--r--lib/user.go45
-rw-r--r--main.go29
-rw-r--r--screens/home.go78
-rw-r--r--screens/screens.go8
12 files changed, 512 insertions, 34 deletions
diff --git a/config/colors.go b/config/colors.go
new file mode 100644
index 0000000..8824511
--- /dev/null
+++ b/config/colors.go
@@ -0,0 +1,15 @@
+package config
+
+import "github.com/charmbracelet/lipgloss"
+
+type ColorConfig struct {
+ Primary lipgloss.AdaptiveColor
+ Text lipgloss.AdaptiveColor
+}
+
+var (
+ Colors = ColorConfig{
+ Primary: lipgloss.AdaptiveColor{Light: "#2F51A2", Dark: "#2F51A2"},
+ Text: lipgloss.AdaptiveColor{Light: "#F5F5F5", Dark: "#F5F5F5"},
+ }
+)
diff --git a/config/constants.go b/config/constants.go
index 47c3ab1..d04a8aa 100644
--- a/config/constants.go
+++ b/config/constants.go
@@ -8,11 +8,15 @@ import (
var (
AppName = "yato"
+ PrettyAppName = "Yato"
Version = "0.1.0"
MALOAuthBaseURL = "https://myanimelist.net/v1/oauth2/authorize"
MALClientID string
MALClientSecret string
MALRedirectURI = "http://localhost:42069/authenticate"
+ MALAPIBaseURL = "https://api.myanimelist.net/v2"
+ JikanAPIBaseURL = "https://api.jikan.moe/v4"
+ ConfigDir, _ = os.UserConfigDir()
)
// These variables will be set by the linker during build
diff --git a/go.mod b/go.mod
index 8b0735c..b87dca5 100644
--- a/go.mod
+++ b/go.mod
@@ -4,12 +4,14 @@ go 1.21.4
require (
github.com/charmbracelet/bubbletea v1.1.0
+ github.com/charmbracelet/lipgloss v0.13.0
+ golang.org/x/image v0.20.0
golang.org/x/term v0.24.0
+ gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
- github.com/charmbracelet/lipgloss v0.13.0 // indirect
github.com/charmbracelet/x/ansi v0.2.3 // indirect
github.com/charmbracelet/x/term v0.2.0 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
@@ -23,6 +25,5 @@ require (
github.com/rivo/uniseg v0.4.7 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.25.0 // indirect
- golang.org/x/text v0.3.8 // indirect
- gopkg.in/yaml.v3 v3.0.1 // indirect
+ golang.org/x/text v0.18.0 // indirect
)
diff --git a/go.sum b/go.sum
index 7ced19a..370deff 100644
--- a/go.sum
+++ b/go.sum
@@ -27,6 +27,8 @@ github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1n
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
+golang.org/x/image v0.20.0 h1:7cVCUjQwfL18gyBJOmYvptfSHS8Fb3YUDtfLIZ7Nbpw=
+golang.org/x/image v0.20.0/go.mod h1:0a88To4CYVBAHp5FXJm8o7QbUl37Vd85ply1vyD8auM=
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-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -39,6 +41,8 @@ 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.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
+golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
+golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/utils/auth.go b/lib/auth.go
index 7745237..b5bc06a 100644
--- a/utils/auth.go
+++ b/lib/auth.go
@@ -1,4 +1,4 @@
-package utils
+package lib
import (
"crypto/rand"
@@ -8,6 +8,8 @@ import (
"io"
"net/http"
"net/url"
+ "os/exec"
+ "runtime"
"strings"
"yato/config"
)
@@ -34,6 +36,23 @@ func GetOAuthURL(codeVerifier string) string {
config.MALOAuthBaseURL, config.MALClientID, config.MALRedirectURI, codeVerifier)
}
+func OpenBrowser(url string) error {
+ var err error
+
+ switch runtime.GOOS {
+ case "linux":
+ err = exec.Command("xdg-open", url).Start()
+ case "windows":
+ err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
+ case "darwin":
+ err = exec.Command("open", url).Start()
+ default:
+ err = fmt.Errorf("unsupported platform")
+ }
+
+ return err
+}
+
func ExchangeToken(code, codeVerifier string) (*config.MyAnimeListConfig, error) {
data := url.Values{}
data.Set("grant_type", "authorization_code")
diff --git a/lib/image_cache.go b/lib/image_cache.go
new file mode 100644
index 0000000..58cd09b
--- /dev/null
+++ b/lib/image_cache.go
@@ -0,0 +1,98 @@
+package lib
+
+import (
+ "fmt"
+ "image"
+ "image/jpeg"
+ "io"
+ "net/http"
+ "os"
+ "path/filepath"
+ "yato/config"
+)
+
+// ImageCache handles caching and retrieving images
+type ImageCache struct {
+ cacheDir string
+}
+
+// NewImageCache creates a new ImageCache
+func NewImageCache() *ImageCache {
+ cacheDir := filepath.Join(config.ConfigDir, config.AppName, "cache")
+ return &ImageCache{cacheDir: cacheDir}
+}
+
+// GetImage retrieves an image, either from cache or by downloading it
+func (c *ImageCache) GetImage(mediaType string, malID int, size string, url string) (image.Image, error) {
+ cachePath := c.getCachePath(mediaType, malID, size)
+
+ // Check if the image is already cached
+ if img, err := c.loadFromCache(cachePath); err == nil {
+ return img, nil
+ }
+
+ // If not cached, download and cache the image
+ return c.downloadAndCache(url, cachePath)
+}
+
+func (c *ImageCache) getCachePath(mediaType string, malID int, size string) string {
+ return filepath.Join(c.cacheDir, mediaType, fmt.Sprintf("%d", malID), size+".jpg")
+}
+
+func (c *ImageCache) loadFromCache(path string) (image.Image, error) {
+ file, err := os.Open(path)
+ if err != nil {
+ return nil, err
+ }
+ defer file.Close()
+
+ img, err := jpeg.Decode(file)
+ if err != nil {
+ return nil, err
+ }
+
+ return img, nil
+}
+
+func (c *ImageCache) downloadAndCache(url, cachePath string) (image.Image, error) {
+ resp, err := http.Get(url)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("failed to download image: %s", resp.Status)
+ }
+
+ // Ensure the cache directory exists
+ if err := os.MkdirAll(filepath.Dir(cachePath), 0755); err != nil {
+ return nil, err
+ }
+
+ // Create the cache file
+ cacheFile, err := os.Create(cachePath)
+ if err != nil {
+ return nil, err
+ }
+ defer cacheFile.Close()
+
+ // Download and write to cache file
+ _, err = io.Copy(cacheFile, resp.Body)
+ if err != nil {
+ return nil, err
+ }
+
+ // Reset file pointer and decode the image
+ _, err = cacheFile.Seek(0, 0)
+ if err != nil {
+ return nil, err
+ }
+
+ img, err := jpeg.Decode(cacheFile)
+ if err != nil {
+ return nil, err
+ }
+
+ return img, nil
+}
diff --git a/lib/image_renderer.go b/lib/image_renderer.go
new file mode 100644
index 0000000..03a8059
--- /dev/null
+++ b/lib/image_renderer.go
@@ -0,0 +1,158 @@
+package lib
+
+import (
+ "bytes"
+ "encoding/base64"
+ "fmt"
+ "image"
+ "image/color"
+ "image/jpeg"
+ "image/png"
+ "os"
+ "strings"
+
+ "golang.org/x/image/draw"
+)
+
+type ImageRenderer struct {
+ method string
+}
+
+func NewImageRenderer() *ImageRenderer {
+ method := determineRenderMethod()
+ return &ImageRenderer{method: method}
+}
+
+func determineRenderMethod() string {
+ if os.Getenv("TERM") == "xterm-kitty" {
+ return "kitty"
+ } else if os.Getenv("TERM_PROGRAM") == "iTerm.app" {
+ return "iterm2"
+ } else if os.Getenv("TERM") == "xterm-256color" && os.Getenv("VTE_VERSION") != "" {
+ return "sixel"
+ }
+ return "none"
+}
+
+func (r *ImageRenderer) RenderImage(img image.Image, width, height int) string {
+ switch r.method {
+ case "kitty":
+ return r.renderKitty(img, width, height)
+ case "iterm2":
+ return r.renderITerm2(img, width, height)
+ case "sixel":
+ return r.renderSixel(img, width, height)
+ case "ascii":
+ return r.renderASCII(img, width, height)
+ default:
+ return ""
+ }
+}
+
+func (r *ImageRenderer) renderKitty(img image.Image, width, height int) string {
+ resized := image.NewRGBA(image.Rect(0, 0, width, height))
+ draw.NearestNeighbor.Scale(resized, resized.Rect, img, img.Bounds(), draw.Over, nil)
+
+ var buf bytes.Buffer
+ png.Encode(&buf, resized)
+ encoded := base64.StdEncoding.EncodeToString(buf.Bytes())
+
+ // Split the encoded data into chunks
+ const chunkSize = 4096
+ chunks := make([]string, 0, (len(encoded)+chunkSize-1)/chunkSize)
+ for i := 0; i < len(encoded); i += chunkSize {
+ end := i + chunkSize
+ if end > len(encoded) {
+ end = len(encoded)
+ }
+ chunks = append(chunks, encoded[i:end])
+ }
+
+ // Build the Kitty graphics protocol command
+ var result strings.Builder
+ for i, chunk := range chunks {
+ if i == 0 {
+ result.WriteString(fmt.Sprintf("\033_Ga=T,f=100,s=%d,v=%d,m=1;", width, height))
+ } else {
+ result.WriteString("\033_Gm=1;")
+ }
+ result.WriteString(chunk)
+ result.WriteString("\033\\")
+ }
+
+ // Final chunk
+ result.WriteString("\033_Gm=0;\033\\")
+
+ return result.String()
+}
+
+func (r *ImageRenderer) renderITerm2(img image.Image, width, height int) string {
+ // Implement iTerm2 inline image protocol
+ var buf bytes.Buffer
+ jpeg.Encode(&buf, img, nil)
+ encoded := base64.StdEncoding.EncodeToString(buf.Bytes())
+ return fmt.Sprintf("\033]1337;File=inline=1;width=%dpx;height=%dpx:%s\a", width, height, encoded)
+}
+
+func (r *ImageRenderer) renderSixel(img image.Image, width, height int) string {
+ resized := image.NewRGBA(image.Rect(0, 0, width, height))
+ draw.NearestNeighbor.Scale(resized, resized.Rect, img, img.Bounds(), draw.Over, nil)
+
+ // Convert to Sixel
+ var sb strings.Builder
+ sb.WriteString("\033Pq") // Start Sixel sequence
+ sb.WriteString("\"1;1;") // Set color mode and aspect ratio
+ sb.WriteString(fmt.Sprintf("%d;%d", width, height))
+ sb.WriteString("\n")
+
+ // Simple color quantization (this can be improved)
+ palette := make(map[color.Color]int)
+ colorIndex := 0
+
+ for y := 0; y < height; y++ {
+ sixelRow := make([]int, width)
+ for x := 0; x < width; x++ {
+ c := resized.At(x, y)
+ if _, exists := palette[c]; !exists {
+ palette[c] = colorIndex
+ colorIndex++
+ r, g, b, _ := c.RGBA()
+ sb.WriteString(fmt.Sprintf("#%d;2;%d;%d;%d", palette[c], r>>8, g>>8, b>>8))
+ }
+ sixelRow[x] = palette[c]
+ }
+
+ // Encode sixel data
+ for i := 0; i < 6; i++ {
+ for _, colorIdx := range sixelRow {
+ sb.WriteByte(byte('?' + ((colorIdx >> i) & 1)))
+ }
+ sb.WriteByte('-')
+ }
+ sb.WriteByte('\n')
+ }
+
+ sb.WriteString("\033\\") // End Sixel sequence
+ return sb.String()
+}
+
+func (r *ImageRenderer) renderASCII(img image.Image, width, height int) string {
+ // Implement a simple ASCII art renderer
+ // This is a very basic implementation and can be improved
+ bounds := img.Bounds()
+ ascii := ""
+ for y := bounds.Min.Y; y < bounds.Max.Y; y += height / 10 {
+ for x := bounds.Min.X; x < bounds.Max.X; x += width / 20 {
+ c := img.At(x, y)
+ r, g, b, _ := c.RGBA()
+ avg := (r + g + b) / 3
+ if avg > 32768 {
+ ascii += " "
+ } else {
+ ascii += "#"
+ }
+ }
+ ascii += "\n"
+ }
+ return ascii
+}
diff --git a/lib/recommendations.go b/lib/recommendations.go
new file mode 100644
index 0000000..6875a1f
--- /dev/null
+++ b/lib/recommendations.go
@@ -0,0 +1,79 @@
+package lib
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "yato/config"
+)
+
+type Recommendation struct {
+ MALId string `json:"mal_id"`
+ URL string `json:"url"`
+ Entry []struct {
+ MALId int `json:"mal_id"`
+ URL string `json:"url"`
+ Images struct {
+ JPG struct {
+ ImageURL string `json:"image_url"`
+ SmallImageURL string `json:"small_image_url"`
+ LargeImageURL string `json:"large_image_url"`
+ } `json:"jpg"`
+ WebP struct {
+ ImageURL string `json:"image_url"`
+ SmallImageURL string `json:"small_image_url"`
+ LargeImageURL string `json:"large_image_url"`
+ } `json:"webp"`
+ } `json:"images"`
+ Title string `json:"title"`
+ } `json:"entry"`
+ Content string `json:"content"`
+ Date string `json:"date"`
+ User struct {
+ URL string `json:"url"`
+ Username string `json:"username"`
+ } `json:"user"`
+}
+
+type recommendationsResponse struct {
+ Pagination struct {
+ LastVisiblePage int `json:"last_visible_page"`
+ HasNextPage bool `json:"has_next_page"`
+ } `json:"pagination"`
+ Data []Recommendation `json:"data"`
+}
+
+func getRecentRecommendations(mediaType string) ([]Recommendation, error) {
+ url := fmt.Sprintf("%s/recommendations/%s", config.JikanAPIBaseURL, mediaType)
+
+ req, err := http.NewRequest("GET", url, nil)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create request: %w", err)
+ }
+
+ client := &http.Client{}
+ resp, err := client.Do(req)
+ if err != nil {
+ return nil, fmt.Errorf("failed to send request: %w", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
+ }
+
+ var response recommendationsResponse
+ if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
+ return nil, fmt.Errorf("failed to decode response: %w", err)
+ }
+
+ return response.Data, nil
+}
+
+func GetRecentAnimeRecommendations() ([]Recommendation, error) {
+ return getRecentRecommendations("anime")
+}
+
+func GetRecentMangaRecommendations() ([]Recommendation, error) {
+ return getRecentRecommendations("manga")
+}
diff --git a/lib/user.go b/lib/user.go
new file mode 100644
index 0000000..cbfde00
--- /dev/null
+++ b/lib/user.go
@@ -0,0 +1,45 @@
+package lib
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "yato/config"
+)
+
+type MALUser struct {
+ ID int `json:"id"`
+ Name string `json:"name"`
+ Birthday string `json:"birthday"`
+ Location string `json:"location"`
+ JoinedAt string `json:"joined_at"`
+ Picture string `json:"picture"`
+}
+
+func CurrentUser() (*MALUser, error) {
+ var user MALUser
+
+ req, err := http.NewRequest("GET", "https://api.myanimelist.net/v2/users/@me", nil)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create request: %w", err)
+ }
+ req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", config.GetConfig().MyAnimeList.AccessToken))
+
+ client := &http.Client{}
+ resp, err := client.Do(req)
+ if err != nil {
+ return nil, fmt.Errorf("failed to send request: %w", err)
+ }
+
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
+ }
+
+ if err := json.NewDecoder(resp.Body).Decode(&user); err != nil {
+ return nil, fmt.Errorf("failed to decode response: %w", err)
+ }
+
+ return &user, nil
+}
diff --git a/main.go b/main.go
index 4764a3c..caeff9c 100644
--- a/main.go
+++ b/main.go
@@ -6,11 +6,9 @@ import (
"log"
"net/http"
"os"
- "os/exec"
- "runtime"
"yato/config"
+ "yato/lib"
"yato/screens"
- "yato/utils"
tea "github.com/charmbracelet/bubbletea"
)
@@ -50,12 +48,12 @@ func StartApp() {
func StartOAuthFlow() {
var err error
- codeVerifier, err = utils.GetNewCodeVerifier()
+ codeVerifier, err = lib.GetNewCodeVerifier()
if err != nil {
log.Fatalf("failed to generate code verifier: %s", err)
}
- url := utils.GetOAuthURL(codeVerifier)
+ url := lib.GetOAuthURL(codeVerifier)
server := &http.Server{Addr: ":42069"}
http.HandleFunc("/authenticate", handleOAuthCallback)
@@ -66,7 +64,7 @@ func StartOAuthFlow() {
}
}()
- if err := openBrowser(url); err != nil {
+ if err := lib.OpenBrowser(url); err != nil {
log.Printf("failed to open browser: %v. Visit %s in your browser to authenticate.", err, url)
}
@@ -89,7 +87,7 @@ func handleOAuthCallback(w http.ResponseWriter, r *http.Request) {
return
}
- malConfig, err := utils.ExchangeToken(code, codeVerifier)
+ malConfig, err := lib.ExchangeToken(code, codeVerifier)
if err != nil {
errorChan <- fmt.Errorf("failed to exchange token: %w", err)
http.Error(w, "failed to exchange token", http.StatusInternalServerError)
@@ -106,20 +104,3 @@ func handleOAuthCallback(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Authentication successful! You can now close this tab."))
authorizationChan <- struct{}{}
}
-
-func openBrowser(url string) error {
- var err error
-
- switch runtime.GOOS {
- case "linux":
- err = exec.Command("xdg-open", url).Start()
- case "windows":
- err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
- case "darwin":
- err = exec.Command("open", url).Start()
- default:
- err = fmt.Errorf("unsupported platform")
- }
-
- return err
-}
diff --git a/screens/home.go b/screens/home.go
index 64e4705..fa88fb5 100644
--- a/screens/home.go
+++ b/screens/home.go
@@ -1,12 +1,33 @@
package screens
-import tea "github.com/charmbracelet/bubbletea"
+import (
+ "fmt"
+ "yato/config"
+ "yato/lib"
+
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/lipgloss"
+)
type HomeScreen struct {
+ RecentAnimeRecommendations []lib.Recommendation
+ RecentMangaRecommendations []lib.Recommendation
+ imageCache *lib.ImageCache
+ imageRenderer *lib.ImageRenderer
}
func homeScreen() tea.Model {
- return HomeScreen{}
+ recentAnimeRecommendations, _ := lib.GetRecentAnimeRecommendations()
+ recentMangaRecommendations, _ := lib.GetRecentMangaRecommendations()
+ imageCache := lib.NewImageCache()
+ imageRenderer := lib.NewImageRenderer()
+
+ return HomeScreen{
+ RecentAnimeRecommendations: recentAnimeRecommendations,
+ RecentMangaRecommendations: recentMangaRecommendations,
+ imageCache: imageCache,
+ imageRenderer: imageRenderer,
+ }
}
func (h HomeScreen) Init() tea.Cmd {
@@ -15,7 +36,7 @@ func (h HomeScreen) Init() tea.Cmd {
func (h HomeScreen) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if msg, ok := msg.(tea.KeyMsg); ok {
- if msg.Type == tea.KeyCtrlC {
+ if msg.String() == "q" {
return h, tea.Quit
}
}
@@ -24,5 +45,54 @@ func (h HomeScreen) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
func (h HomeScreen) View() string {
- return "Home"
+ w := lipgloss.Width
+
+ // Top bar, Content, Status bar
+ topBarStyle := lipgloss.NewStyle().
+ Foreground(config.Colors.Text).
+ Background(config.Colors.Primary)
+
+ mainText := topBarStyle.Padding(0, 0, 0, 1).Render(config.PrettyAppName + " | [H]ome | [A]nime | [M]anga | [S]earch | [C]ommunity | [P]rofile | [O]ptions | [Q]uit")
+ userText := topBarStyle.Padding(0, 1, 0, 0).Render("User: " + globals.CurrentUser.Name + " | [L]ogout")
+ separator := topBarStyle.Width(globals.width - w(mainText) - w(userText)).Render("")
+
+ topBar := lipgloss.JoinHorizontal(
+ lipgloss.Top,
+ mainText,
+ separator,
+ userText,
+ )
+
+ content := ""
+ // Top 5 recommendations
+ for i, rec := range h.RecentAnimeRecommendations {
+ if i == 5 {
+ break
+ }
+ content += h.renderRecommendation("anime", rec)
+ }
+
+ for i, rec := range h.RecentMangaRecommendations {
+ if i == 5 {
+ break
+ }
+ content += h.renderRecommendation("manga", rec)
+ }
+
+ return lipgloss.JoinVertical(
+ lipgloss.Top,
+ topBar,
+ content,
+ )
+
+}
+
+func (h HomeScreen) renderRecommendation(mediaType string, rec lib.Recommendation) string {
+ img, err := h.imageCache.GetImage(mediaType, rec.Entry[0].MALId, "small", rec.Entry[0].Images.JPG.SmallImageURL)
+ if err != nil {
+ return fmt.Sprintf("%s -> %s\n", rec.Entry[0].Title, rec.Entry[1].Title)
+ }
+
+ renderedImage := h.imageRenderer.RenderImage(img, 20, 30)
+ return fmt.Sprintf("%s%s -> %s\n", renderedImage, rec.Entry[0].Title, rec.Entry[1].Title)
}
diff --git a/screens/screens.go b/screens/screens.go
index 1675ec7..f04878f 100644
--- a/screens/screens.go
+++ b/screens/screens.go
@@ -2,6 +2,7 @@ package screens
import (
"os"
+ "yato/lib"
tea "github.com/charmbracelet/bubbletea"
"golang.org/x/term"
@@ -12,8 +13,9 @@ type ScreenSwitcher struct {
}
type Globals struct {
- width int
- height int
+ width int
+ height int
+ CurrentUser *lib.MALUser
}
var globals Globals
@@ -65,5 +67,7 @@ func Initialize() tea.Model {
globals.width = width
globals.height = height
+ globals.CurrentUser, _ = lib.CurrentUser()
+
return screen()
}