diff options
Diffstat (limited to 'pkg/ui/views/play.go')
-rw-r--r-- | pkg/ui/views/play.go | 483 |
1 files changed, 0 insertions, 483 deletions
diff --git a/pkg/ui/views/play.go b/pkg/ui/views/play.go index 3997a88..6ebebb3 100644 --- a/pkg/ui/views/play.go +++ b/pkg/ui/views/play.go @@ -1,18 +1,11 @@ package views import ( - "encoding/json" "fmt" - "net/http" - "os" - "strconv" "strings" "github.com/boozec/rahanna/internal/api/database" - "github.com/boozec/rahanna/internal/logger" - "github.com/boozec/rahanna/internal/network" "github.com/boozec/rahanna/pkg/ui/multiplayer" - "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/paginator" "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" @@ -36,67 +29,6 @@ A B C D E F G H ` ) -type PlayModelPage int - -const ( - LandingPage PlayModelPage = iota - InsertCodePage - StartGamePage -) - -type responseOk struct { - Name string `json:"name"` - GameID int `json:"id"` - IP string `json:"ip"` - Port int `json:"int"` -} - -// API response types -type playResponse struct { - Ok responseOk - Error string `json:"error"` -} - -// Keyboard controls -type playKeyMap struct { - EnterNewGame key.Binding - StartNewGame key.Binding - GoLogout key.Binding - Quit key.Binding - NextPage key.Binding - PrevPage key.Binding -} - -// Default key bindings for the play model -var defaultPlayKeyMap = playKeyMap{ - EnterNewGame: key.NewBinding( - key.WithKeys("alt+E", "alt+e"), - key.WithHelp("Alt+E", "Enter a play using code"), - ), - StartNewGame: key.NewBinding( - key.WithKeys("alt+s", "alt+S"), - key.WithHelp("Alt+S", "Start a new play"), - ), - GoLogout: key.NewBinding( - key.WithKeys("alt+Q", "alt+q"), - key.WithHelp("Alt+Q", "Logout"), - ), - Quit: key.NewBinding( - key.WithKeys("Q", "q"), - key.WithHelp(" Q", "Quit"), - ), - NextPage: key.NewBinding( - key.WithKeys("right"), - key.WithHelp("→/h", "Next Page"), - ), - PrevPage: key.NewBinding( - key.WithKeys("left"), - key.WithHelp("←/l", "Prev Page"), - ), -} - -type StartGameMsg struct{} - type PlayModel struct { // UI dimensions width int @@ -136,19 +68,6 @@ func NewPlayModel(width, height int) PlayModel { } } -// Create and configure the name input prompt -func createNamePrompt(width int) textinput.Model { - namePrompt := textinput.New() - namePrompt.Prompt = " " - namePrompt.TextStyle = inputStyle - namePrompt.Placeholder = "rectangular-lake" - namePrompt.Focus() - namePrompt.CharLimit = 23 - namePrompt.Width = getFormWidth(width) - - return namePrompt -} - func (m PlayModel) Init() tea.Cmd { ClearScreen() return tea.Batch(textinput.Blink, m.fetchGames()) @@ -206,405 +125,3 @@ func (m PlayModel) View() string { centeredContent, ) } - -func (m PlayModel) handleWindowSize(msg tea.WindowSizeMsg) (tea.Model, tea.Cmd) { - m.width = msg.Width - m.height = msg.Height - return m, nil -} - -func (m PlayModel) handleKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) { - var cmd tea.Cmd - - switch { - case key.Matches(msg, m.keys.EnterNewGame): - m.page = InsertCodePage - m.namePrompt, cmd = m.namePrompt.Update("suca") - return m, cmd - - case key.Matches(msg, m.keys.StartNewGame): - m.page = StartGamePage - if !m.isLoading { - m.isLoading = true - return m, m.newGameCallback() - } - - case key.Matches(msg, m.keys.GoLogout): - return m, logout(m.width, m.height+1) - - case key.Matches(msg, m.keys.Quit): - return m, tea.Quit - - case msg.Type == tea.KeyEnter: - if m.page == InsertCodePage && !m.isLoading { - m.isLoading = true - return m, m.enterGame() - } - } - - m.paginator, _ = m.paginator.Update(msg) - - if m.page == InsertCodePage { - m.namePrompt, cmd = m.namePrompt.Update(msg) - return m, cmd - } - - return m, nil -} - -func (m *PlayModel) handlePlayResponse(msg playResponse) (tea.Model, tea.Cmd) { - m.isLoading = false - m.err = nil - - if msg.Error != "" { - m.err = fmt.Errorf("%s", msg.Error) - if msg.Error == "unauthorized" { - return m, logout(m.width, m.height+1) - } - } else { - m.playName = msg.Ok.Name - m.currentGameId = msg.Ok.GameID - logger, _ := logger.GetLogger() - - callbackCompleted := make(chan bool) - m.network = multiplayer.NewGameNetwork("peer-1", fmt.Sprintf("%s:%d", msg.Ok.IP, msg.Ok.Port), func() error { - close(callbackCompleted) - return nil - }, logger) - - return m, func() tea.Msg { - <-callbackCompleted - return StartGameMsg{} - } - } - - return m, nil -} - -func (m *PlayModel) handleGameResponse(msg database.Game) (tea.Model, tea.Cmd) { - m.isLoading = false - m.game = &msg - m.err = nil - ip := strings.Split(m.game.IP2, ":") - if len(ip) == 2 { - localIP := ip[0] - localPort, _ := strconv.ParseInt(ip[1], 10, 32) - - logger, _ := logger.GetLogger() - network := multiplayer.NewGameNetwork("peer-2", fmt.Sprintf("%s:%d", localIP, localPort), func() error { - return nil - }, logger) - - return m, SwitchModelCmd(NewGameModel(m.width, m.height+1, "peer-1", m.game.ID, network)) - } - return m, nil -} - -func (m *PlayModel) handleGamesResponse(msg []database.Game) (tea.Model, tea.Cmd) { - m.isLoading = false - m.games = msg - m.err = nil - m.paginator.SetTotalPages(len(m.games)) - return m, nil -} - -func (m *PlayModel) handleError(msg error) (tea.Model, tea.Cmd) { - m.isLoading = false - m.err = msg - return m, nil -} - -func (m PlayModel) renderPageContent(base lipgloss.Style) string { - switch m.page { - case LandingPage: - m.namePrompt.Blur() - if len(m.games) == 0 { - return base.Render(chessBoard) - } else { - start, end := m.paginator.GetSliceBounds(len(m.games)) - gamesStrings := formatGamesForPage(m.games[start:end], altCodeStyle) - pageInfo := m.paginator.View() - return base.Render(lipgloss.JoinVertical(lipgloss.Center, strings.Join(gamesStrings, "\n"), pageInfo)) - } - case InsertCodePage: - return m.renderInsertCodeContent(base) - - case StartGamePage: - return m.renderStartGameContent(base) - } - - return "" -} - -func formatGamesForPage(games []database.Game, altCodeStyle lipgloss.Style) []string { - var gamesStrings []string - gamesStrings = append(gamesStrings, "Games list") - - longestName := 0 - for _, game := range games { - if len(game.Name) > longestName { - longestName = len(game.Name) - } - } - - for i, game := range games { - indexStr := altCodeStyle.Render(fmt.Sprintf("[%d] ", i)) - nameStr := game.Name - dateStr := game.UpdatedAt.Format("2006-01-02 15:04") - - padding := longestName - len(nameStr) - paddingStr := strings.Repeat(" ", padding+4) - - line := lipgloss.JoinHorizontal(lipgloss.Left, - indexStr, - nameStr, - paddingStr, - lipgloss.NewStyle().Foreground(lipgloss.Color("#d35400")).Render(dateStr), - ) - gamesStrings = append(gamesStrings, line) - } - return gamesStrings -} - -func (m PlayModel) renderInsertCodeContent(base lipgloss.Style) string { - // When loading, show loading status - if m.isLoading { - return base.Render( - lipgloss.NewStyle(). - Align(lipgloss.Center). - Bold(true). - Render("Loading..."), - ) - } - - // Default: show input prompt - return base.Render( - lipgloss.JoinVertical(lipgloss.Left, - lipgloss.NewStyle().Render("Insert play code:"), - m.namePrompt.View(), - lipgloss.NewStyle(). - Align(lipgloss.Center). - PaddingTop(2). - Bold(true). - Render(fmt.Sprintf("Press %s to join", - lipgloss.NewStyle().Italic(true).Render("Enter"))), - ), - ) -} - -func (m PlayModel) renderStartGameContent(base lipgloss.Style) string { - var statusMsg string - - if m.isLoading { - statusMsg = "Loading..." - } else if m.playName != "" { - gameCode := lipgloss.NewStyle(). - Italic(true). - Foreground(lipgloss.Color("#F39C12")). - Render(m.playName) - - statusMsg = fmt.Sprintf("Share `%s` to your friend", gameCode) - } - - return base.Render(statusMsg) -} - -func (m PlayModel) buildWindowContent(content string, formWidth int) string { - if m.err != nil { - formError := fmt.Sprintf("Error: %v", m.err.Error()) - return lipgloss.JoinVertical( - lipgloss.Center, - windowStyle.Width(formWidth).Render(lipgloss.JoinVertical( - lipgloss.Center, - errorStyle.Align(lipgloss.Center).Width(formWidth-4).Render(formError), - content, - )), - ) - } - - return lipgloss.JoinVertical( - lipgloss.Center, - windowStyle.Width(formWidth).Render(lipgloss.JoinVertical( - lipgloss.Center, - content, - )), - ) -} - -func (m PlayModel) renderNavigationButtons() string { - logoutKey := fmt.Sprintf("%s %s", - altCodeStyle.Render(m.keys.GoLogout.Help().Key), - m.keys.GoLogout.Help().Desc) - - quitKey := fmt.Sprintf("%s %s", - altCodeStyle.Render(m.keys.Quit.Help().Key), - m.keys.Quit.Help().Desc) - - if m.page == LandingPage { - enterKey := fmt.Sprintf("%s %s", - altCodeStyle.Render(m.keys.EnterNewGame.Help().Key), - m.keys.EnterNewGame.Help().Desc) - - startKey := fmt.Sprintf("%s %s", - altCodeStyle.Render(m.keys.StartNewGame.Help().Key), - m.keys.StartNewGame.Help().Desc) - - nextPageKey := fmt.Sprintf("%s %s", - altCodeStyle.Render(m.keys.NextPage.Help().Key), - m.keys.NextPage.Help().Desc) - - prevPageKey := fmt.Sprintf("%s %s", - altCodeStyle.Render(m.keys.PrevPage.Help().Key), - m.keys.PrevPage.Help().Desc) - - return lipgloss.JoinVertical( - lipgloss.Left, - enterKey, - startKey, - lipgloss.JoinHorizontal(lipgloss.Left, prevPageKey, " | ", nextPageKey), - logoutKey, - quitKey, - ) - } - - return lipgloss.JoinVertical( - lipgloss.Left, - logoutKey, - quitKey, - ) -} - -func (m *PlayModel) newGameCallback() tea.Cmd { - return func() tea.Msg { - // Get authorization token - authorization, err := getAuthorizationToken() - if err != nil { - return playResponse{Error: err.Error()} - } - - // Set up network connection - port, err := network.GetRandomAvailablePort() - if err != nil { - return playResponse{Error: err.Error()} - } - - ip := network.GetOutboundIP().String() - // FIXME: ip - ip = "0.0.0.0" - - // Prepare request payload - payload, err := json.Marshal(map[string]string{ - "ip": fmt.Sprintf("%s:%d", ip, port), - }) - if err != nil { - return playResponse{Error: err.Error()} - } - - // Send API request - url := os.Getenv("API_BASE") + "/play" - resp, err := sendAPIRequest("POST", url, payload, authorization) - if err != nil { - return playResponse{Error: err.Error()} - } - defer resp.Body.Close() - - // Handle response - if resp.StatusCode != http.StatusOK { - var response playResponse - if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { - return playResponse{Error: fmt.Sprintf("HTTP error: %d, unable to decode body", resp.StatusCode)} - } - return playResponse{Error: response.Error} - } - - // Decode successful response - var response struct { - Name string `json:"name"` - ID int `json:"id"` - Error string `json:"error"` - } - if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { - return playResponse{Error: fmt.Sprintf("Error decoding JSON: %v", err)} - } - - return playResponse{Ok: responseOk{Name: response.Name, GameID: response.ID, IP: ip, Port: port}} - } -} - -func (m PlayModel) enterGame() tea.Cmd { - return func() tea.Msg { - // Get authorization token - authorization, err := getAuthorizationToken() - if err != nil { - return playResponse{Error: err.Error()} - } - - // Set up network connection - port, err := network.GetRandomAvailablePort() - if err != nil { - return playResponse{Error: err.Error()} - } - - ip := network.GetOutboundIP().String() - // FIXME: ip - ip = "0.0.0.0" - - // Prepare request payload - payload, err := json.Marshal(map[string]string{ - "ip": fmt.Sprintf("%s:%d", ip, port), - "name": m.namePrompt.Value(), - }) - if err != nil { - return playResponse{Error: err.Error()} - } - - // Send API request - url := os.Getenv("API_BASE") + "/enter-game" - resp, err := sendAPIRequest("POST", url, payload, authorization) - if err != nil { - return playResponse{Error: err.Error()} - } - defer resp.Body.Close() - - // Handle response - if resp.StatusCode != http.StatusOK { - var response playResponse - if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { - return playResponse{Error: fmt.Sprintf("HTTP error: %d, unable to decode body", resp.StatusCode)} - } - return playResponse{Error: response.Error} - } - - // Decode successful response - var response database.Game - if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { - return playResponse{Error: fmt.Sprintf("Error decoding JSON: %v", err)} - } - - return response - } -} - -func (m *PlayModel) fetchGames() tea.Cmd { - return func() tea.Msg { - var games []database.Game - // Get authorization token - authorization, err := getAuthorizationToken() - if err != nil { - return games - } - - // Send API request - url := os.Getenv("API_BASE") + "/play" - resp, err := sendAPIRequest("GET", url, nil, authorization) - if err != nil { - return games - } - defer resp.Body.Close() - - if err := json.NewDecoder(resp.Body).Decode(&games); err != nil { - return []database.Game{} - } - return games - } -} |