diff --git a/README.md b/README.md index fe16fca..8fb7991 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ - Просмотр деталей треда и комментариев - Поддержка изображений и видео - Темная/светлая тема -- Навигация с кнопкой "Назад" (index lore) +- Навигация с кнопкой "Назад" - Улучшенный заголовок с статическими кнопками - Система настроек с сохранением: - Тема (темная/светлая) @@ -17,22 +17,58 @@ - Автообновление - Показ файлов - Компактный режим -- Поддержка Android и iOS (с сохранением настроек (блять нахуй я туда полез)) +- **Полная поддержка постинга:** + - Аутентификация по ключу + - Аутентификация по passcode + - Создание тредов + - Добавление комментариев + - Автоматическое обновление после постинга + - Кнопка "Обновить" для ручного обновления +- Поддержка Android и iOS - **Оптимизации для мобильных устройств:** - Кэширование данных для быстрой загрузки - Дебаунсинг UI обновлений - Оптимизация потребления батареи - Агрессивная сборка мусора на мобильных - Ограничение частоты сетевых запросов - - **Многостраничность для списка тредов:** - - Настраиваемый размер страницы (5-20 тредов) - - Навигация между страницами - - Ограничение тредов для больших досок - - **Управление кэшем:** - - Очистка кэша досок - - Очистка кэша тредов для всех досок - - Очистка кэша деталей тредов - - Очистка всего кэша +- **Многостраничность для списка тредов:** + - Настраиваемый размер страницы (5-20 тредов) + - Навигация между страницами + - Ограничение тредов для больших досок +- **Управление кэшем:** + - Очистка кэша досок + - Очистка кэша тредов для всех досок + - Очистка кэша деталей тредов + - Очистка всего кэша + +## Аутентификация и постинг + +### Настройка аутентификации + +1. Откройте настройки в приложении +2. Введите ключ аутентификации (если есть) +3. Введите passcode для постинга (если есть) +4. Используйте кнопки "Тест ключа" и "Тест passcode" для проверки + +### Создание тредов + +1. Перейдите в нужную доску +2. Нажмите "Создать тред" +3. Заполните заголовок и текст +4. Нажмите "Создать тред" + +### Добавление комментариев + +1. Откройте тред +2. Нажмите "Добавить комментарий" +3. Введите текст комментария +4. Нажмите "Добавить комментарий" + +### Автоматическое обновление + +- После создания треда автоматически обновляется список тредов +- После добавления комментария автоматически обновляется список комментариев +- Используйте кнопку "Обновить" для принудительного обновления ## Сборка @@ -97,7 +133,6 @@ open MobileMkch.xcodeproj **✅ iOS И Android сборка протестирована и работает!** - ## Требования - Go 1.24+ @@ -109,12 +144,13 @@ open MobileMkch.xcodeproj - Go 1.24+ - Fyne v2.6.2 -- HTTP клиент для API +- HTTP клиент для API с поддержкой сессий +- Система кэширования с TTL ## Структура - `main.go` - точка входа -- `api/client.go` - HTTP клиент для mkch API +- `api/client.go` - HTTP клиент для mkch API с поддержкой аутентификации - `models/models.go` - структуры данных - `settings/settings.go` - система настроек - `cache/cache.go` - система кэширования с поддержкой пагинации @@ -122,7 +158,9 @@ open MobileMkch.xcodeproj - `manager.go` - управление экранами - `boards_screen.go` - список досок - `threads_screen.go` - треды доски - - `thread_detail_screen.go` - детали треда - - `settings_screen.go` - экран настроек + - `thread_detail_screen.go` - детали треда с кнопкой обновления + - `create_thread_screen.go` - создание тредов + - `add_comment_screen.go` - добавление комментариев + - `settings_screen.go` - экран настроек с тестированием аутентификации - `optimization.go` - утилиты оптимизации - `mobile_optimizations.go` - оптимизации для мобильных устройств \ No newline at end of file diff --git a/api/client.go b/api/client.go index 30bc775..eaa6074 100644 --- a/api/client.go +++ b/api/client.go @@ -1,10 +1,14 @@ package api import ( + "bytes" "encoding/json" "fmt" "io" "net/http" + "net/http/cookiejar" + "net/url" + "regexp" "time" "MobileMkch/cache" @@ -20,20 +24,121 @@ type Client struct { httpClient *http.Client baseURL string debug bool + authKey string + passcode string } func NewClient() *Client { + jar, _ := cookiejar.New(nil) return &Client{ httpClient: &http.Client{ Timeout: 30 * time.Second, + Jar: jar, }, - baseURL: ApiURL, + baseURL: BaseURL, debug: false, } } func (c *Client) EnableDebug(enable bool) { c.debug = enable + if c.debug { + fmt.Printf("[DEBUG] Debug режим включен\n") + } +} + +func (c *Client) Authenticate(authKey string) error { + resp, err := c.httpClient.Get(fmt.Sprintf("%s/key/auth/", c.baseURL)) + if err != nil { + return fmt.Errorf("ошибка получения формы аутентификации: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("ошибка получения формы аутентификации: %d", resp.StatusCode) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("ошибка чтения формы аутентификации: %w", err) + } + + csrfToken := extractCSRFToken(string(body)) + if csrfToken == "" { + return fmt.Errorf("не удалось извлечь CSRF токен для аутентификации") + } + + formData := url.Values{} + formData.Set("csrfmiddlewaretoken", csrfToken) + formData.Set("key", authKey) + + req, err := http.NewRequest("POST", fmt.Sprintf("%s/key/auth/", c.baseURL), bytes.NewBufferString(formData.Encode())) + if err != nil { + return fmt.Errorf("ошибка создания запроса аутентификации: %w", err) + } + + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("Referer", fmt.Sprintf("%s/key/auth/", c.baseURL)) + + resp, err = c.httpClient.Do(req) + if err != nil { + return fmt.Errorf("ошибка отправки аутентификации: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusFound { + return fmt.Errorf("ошибка аутентификации: %d", resp.StatusCode) + } + + c.authKey = authKey + return nil +} + +func (c *Client) LoginWithPasscode(passcode string) error { + resp, err := c.httpClient.Get(fmt.Sprintf("%s/passcode/enter/", c.baseURL)) + if err != nil { + return fmt.Errorf("ошибка получения формы passcode: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("ошибка получения формы passcode: %d", resp.StatusCode) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("ошибка чтения формы passcode: %w", err) + } + + csrfToken := extractCSRFToken(string(body)) + if csrfToken == "" { + return fmt.Errorf("не удалось извлечь CSRF токен для passcode") + } + + formData := url.Values{} + formData.Set("csrfmiddlewaretoken", csrfToken) + formData.Set("passcode", passcode) + + req, err := http.NewRequest("POST", fmt.Sprintf("%s/passcode/enter/", c.baseURL), bytes.NewBufferString(formData.Encode())) + if err != nil { + return fmt.Errorf("ошибка создания запроса passcode: %w", err) + } + + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("Referer", fmt.Sprintf("%s/passcode/enter/", c.baseURL)) + + resp, err = c.httpClient.Do(req) + if err != nil { + return fmt.Errorf("ошибка отправки passcode: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusFound { + return fmt.Errorf("ошибка входа с passcode: %d", resp.StatusCode) + } + + c.passcode = passcode + return nil } func (c *Client) makeRequest(url string) ([]byte, error) { @@ -78,7 +183,7 @@ func (c *Client) GetBoards() ([]models.Board, error) { return boards, nil } - url := c.baseURL + "/boards/" + url := ApiURL + "/boards/" body, err := c.makeRequest(url) if err != nil { return nil, fmt.Errorf("ошибка получения досок: %w", err) @@ -111,7 +216,7 @@ func (c *Client) GetThreads(boardCode string) ([]models.Thread, error) { return threads, nil } - url := fmt.Sprintf("%s/board/%s", c.baseURL, boardCode) + url := fmt.Sprintf("%s/board/%s", ApiURL, boardCode) body, err := c.makeRequest(url) if err != nil { return nil, fmt.Errorf("ошибка получения тредов доски %s: %w", boardCode, err) @@ -126,13 +231,6 @@ func (c *Client) GetThreads(boardCode string) ([]models.Thread, error) { return nil, fmt.Errorf("ошибка парсинга тредов: %w", err) } - // if len(threads) > 12 { - // if c.debug { - // fmt.Printf("[API] Ограничиваем количество тредов с %d до 5\n", len(threads)) - // } - // threads = threads[:12] - // } - cache.GetCache().SetThreads(boardCode, threads) if c.debug { @@ -150,7 +248,7 @@ func (c *Client) GetThread(boardCode string, threadID int) (*models.ThreadDetail return thread, nil } - url := fmt.Sprintf("%s/board/%s/thread/%d", c.baseURL, boardCode, threadID) + url := fmt.Sprintf("%s/board/%s/thread/%d", ApiURL, boardCode, threadID) body, err := c.makeRequest(url) if err != nil { return nil, fmt.Errorf("ошибка получения треда %d: %w", threadID, err) @@ -182,7 +280,7 @@ func (c *Client) GetComments(boardCode string, threadID int) ([]models.Comment, return comments, nil } - url := fmt.Sprintf("%s/board/%s/thread/%d/comments", c.baseURL, boardCode, threadID) + url := fmt.Sprintf("%s/board/%s/thread/%d/comments", ApiURL, boardCode, threadID) body, err := c.makeRequest(url) if err != nil { return nil, fmt.Errorf("ошибка получения комментариев треда %d: %w", threadID, err) @@ -252,3 +350,133 @@ func (c *Client) GetFullThread(boardCode string, threadID int) (*models.ThreadDe return thread, comments, nil } + +func (c *Client) CreateThread(boardCode, title, text, passcode string) error { + if passcode != "" { + if err := c.LoginWithPasscode(passcode); err != nil { + return fmt.Errorf("ошибка входа с passcode: %w", err) + } + } + + formURL := fmt.Sprintf("%s/boards/board/%s/new", c.baseURL, boardCode) + + resp, err := c.httpClient.Get(formURL) + if err != nil { + return fmt.Errorf("ошибка получения формы: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("ошибка получения формы: %d", resp.StatusCode) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("ошибка чтения формы: %w", err) + } + + csrfToken := extractCSRFToken(string(body)) + if csrfToken == "" { + return fmt.Errorf("не удалось извлечь CSRF токен") + } + + formData := url.Values{} + formData.Set("csrfmiddlewaretoken", csrfToken) + formData.Set("title", title) + formData.Set("text", text) + + req, err := http.NewRequest("POST", formURL, bytes.NewBufferString(formData.Encode())) + if err != nil { + return fmt.Errorf("ошибка создания запроса: %w", err) + } + + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("Referer", formURL) + + resp, err = c.httpClient.Do(req) + if err != nil { + return fmt.Errorf("ошибка отправки: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusFound { + return fmt.Errorf("ошибка сервера: %d", resp.StatusCode) + } + + cache.GetCache().Delete("threads_" + boardCode) + + return nil +} + +func (c *Client) AddComment(boardCode string, threadID int, text, passcode string) error { + if passcode != "" { + if err := c.LoginWithPasscode(passcode); err != nil { + return fmt.Errorf("ошибка входа с passcode: %w", err) + } + } + + formURL := fmt.Sprintf("%s/boards/board/%s/thread/%d/comment", c.baseURL, boardCode, threadID) + + resp, err := c.httpClient.Get(formURL) + if err != nil { + return fmt.Errorf("ошибка получения формы: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("ошибка получения формы: %d", resp.StatusCode) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("ошибка чтения формы: %w", err) + } + + csrfToken := extractCSRFToken(string(body)) + if csrfToken == "" { + return fmt.Errorf("не удалось извлечь CSRF токен") + } + + formData := url.Values{} + formData.Set("csrfmiddlewaretoken", csrfToken) + formData.Set("text", text) + + req, err := http.NewRequest("POST", formURL, bytes.NewBufferString(formData.Encode())) + if err != nil { + return fmt.Errorf("ошибка создания запроса: %w", err) + } + + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("Referer", formURL) + + resp, err = c.httpClient.Do(req) + if err != nil { + return fmt.Errorf("ошибка отправки: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusFound { + return fmt.Errorf("ошибка сервера: %d", resp.StatusCode) + } + + cacheKey := fmt.Sprintf("comments_%d", threadID) + cache.GetCache().Delete(cacheKey) + + return nil +} + +func extractCSRFToken(html string) string { + re := regexp.MustCompile(`name=['"]csrfmiddlewaretoken['"]\s+value=['"]([^'"]+)['"]`) + matches := re.FindStringSubmatch(html) + if len(matches) > 1 { + return matches[1] + } + return "" +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} diff --git a/cache/cache.go b/cache/cache.go index 9b02fc3..3f2e5bc 100644 --- a/cache/cache.go +++ b/cache/cache.go @@ -164,11 +164,11 @@ func (c *Cache) GetThreadsPage(boardCode string, page int) ([]models.Thread, boo func (c *Cache) SetThreadDetail(threadID int, thread *models.ThreadDetail) { data, _ := json.Marshal(thread) - c.Set("thread_"+string(rune(threadID)), data, 3*time.Minute) + c.Set("thread_detail_"+fmt.Sprintf("%d", threadID), data, 3*time.Minute) } func (c *Cache) GetThreadDetail(threadID int) (*models.ThreadDetail, bool) { - data, exists := c.Get("thread_" + string(rune(threadID))) + data, exists := c.Get("thread_detail_" + fmt.Sprintf("%d", threadID)) if !exists { return nil, false } @@ -184,11 +184,11 @@ func (c *Cache) GetThreadDetail(threadID int) (*models.ThreadDetail, bool) { func (c *Cache) SetComments(threadID int, comments []models.Comment) { data, _ := json.Marshal(comments) - c.Set("comments_"+string(rune(threadID)), data, 3*time.Minute) + c.Set("comments_"+fmt.Sprintf("%d", threadID), data, 3*time.Minute) } func (c *Cache) GetComments(threadID int) ([]models.Comment, bool) { - data, exists := c.Get("comments_" + string(rune(threadID))) + data, exists := c.Get("comments_" + fmt.Sprintf("%d", threadID)) if !exists { return nil, false } diff --git a/main.go b/main.go index e915c52..c8ce0d5 100644 --- a/main.go +++ b/main.go @@ -27,6 +27,25 @@ func main() { apiClient := api.NewClient() + appSettings, err := settings.Load() + if err == nil { + if appSettings.Key != "" { + if err := apiClient.Authenticate(appSettings.Key); err != nil { + fmt.Printf("Ошибка аутентификации по ключу: %v\n", err) + } else { + fmt.Println("Аутентификация по ключу успешна") + } + } + + if appSettings.Passcode != "" { + if err := apiClient.LoginWithPasscode(appSettings.Passcode); err != nil { + fmt.Printf("Ошибка входа с passcode: %v\n", err) + } else { + fmt.Println("Вход с passcode успешен") + } + } + } + uiManager := ui.NewUIManager(a, w, apiClient) w.SetContent(uiManager.GetMainContent()) diff --git a/settings/settings.go b/settings/settings.go index e83debe..9218e06 100644 --- a/settings/settings.go +++ b/settings/settings.go @@ -18,6 +18,8 @@ type Settings struct { ShowFiles bool `json:"show_files"` CompactMode bool `json:"compact_mode"` PageSize int `json:"page_size"` + Passcode string `json:"passcode"` + Key string `json:"key"` } var settingsPath string diff --git a/ui/manager.go b/ui/manager.go index baad658..6c61238 100644 --- a/ui/manager.go +++ b/ui/manager.go @@ -4,11 +4,13 @@ import ( "MobileMkch/api" "MobileMkch/models" "MobileMkch/settings" + "fmt" "fyne.io/fyne/v2" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" + "MobileMkch/cache" ) type UIManager struct { @@ -42,6 +44,7 @@ func NewUIManager(app fyne.App, window fyne.Window, apiClient *api.Client) *UIMa AutoRefresh: true, ShowFiles: true, CompactMode: false, + Key: "", } } @@ -186,6 +189,10 @@ func (ui *UIManager) ShowThreadDetail(board *models.Board, thread *models.Thread ui.ShowScreen(threadScreen) } +func (ui *UIManager) PushScreen(screen Screen) { + ui.ShowScreen(screen) +} + func (ui *UIManager) GetAPIClient() *api.Client { return ui.apiClient } @@ -195,14 +202,15 @@ func (ui *UIManager) GetWindow() fyne.Window { } func (ui *UIManager) ShowError(title, message string) { - dialog := widget.NewModalPopUp( + var dialog *widget.PopUp + dialog = widget.NewModalPopUp( container.NewVBox( widget.NewLabel(title), widget.NewLabel(""), widget.NewLabel(message), widget.NewLabel(""), widget.NewButton("OK", func() { - // Закрыть диалог - пока что просто прячем popup + dialog.Hide() }), ), ui.window.Canvas(), @@ -211,14 +219,15 @@ func (ui *UIManager) ShowError(title, message string) { } func (ui *UIManager) ShowInfo(title, message string) { - dialog := widget.NewModalPopUp( + var dialog *widget.PopUp + dialog = widget.NewModalPopUp( container.NewVBox( widget.NewLabel(title), widget.NewLabel(""), widget.NewLabel(message), widget.NewLabel(""), widget.NewButton("OK", func() { - // Закрыть диалог + dialog.Hide() }), ), ui.window.Canvas(), @@ -226,6 +235,32 @@ func (ui *UIManager) ShowInfo(title, message string) { dialog.Show() } +func (ui *UIManager) ShowInfoDialog() { + var dialog *widget.PopUp + + content := container.NewVBox( + widget.NewLabel("Информация о НЕОЖИДАНЫХ проблемах"), + widget.NewLabel(""), + widget.NewLabel("Если тебя направили сюда, то значит\nты попал на НЕИЗВЕДАННЫЕ ТЕРРИТОРИИ"), + widget.NewLabel("ДА ДА, не ослышались, это не ошибка,\nэто особенность"), + widget.NewLabel("Увы, разработчик имиджборда\nвставил палки в колеса"), + widget.NewLabel("И без доната ему, например,\nпостинг работать не будет"), + widget.NewLabel(""), + widget.NewLabel("Увы, постинг не работает без доната"), + widget.NewLabel("а разработчик боится что на его сайте\nбудут спам"), + widget.NewLabel("Вкратце - на сайте работает капча"), + widget.NewLabel("а наличие пасскода ее для вас отключает"), + widget.NewLabel("увы, конфет много,\nно на всех не хватит"), + widget.NewLabel(""), + widget.NewButton("Закрыть", func() { + dialog.Hide() + }), + ) + + dialog = widget.NewModalPopUp(content, ui.window.Canvas()) + dialog.Show() +} + func (ui *UIManager) GetSettings() *settings.Settings { return ui.settings } @@ -234,6 +269,29 @@ func (ui *UIManager) SaveSettings() error { return settings.Save(ui.settings) } +func (ui *UIManager) RefreshCurrentScreen() { + if ui.currentScreen != nil { + ui.currentScreen.OnShow() + } +} + +func (ui *UIManager) ClearCacheForBoard(boardCode string) { + cacheKey := "threads_" + boardCode + cache.GetCache().Delete(cacheKey) +} + +func (ui *UIManager) ClearCacheForThread(threadID int) { + commentsKey := fmt.Sprintf("comments_%d", threadID) + threadDetailKey := fmt.Sprintf("thread_detail_%d", threadID) + + cache.GetCache().Delete(commentsKey) + cache.GetCache().Delete(threadDetailKey) +} + +func (ui *UIManager) GetCurrentScreen() Screen { + return ui.currentScreen +} + func (ui *UIManager) SetLastBoard(boardCode string) { ui.settings.LastBoard = boardCode ui.SaveSettings() @@ -243,6 +301,14 @@ func (ui *UIManager) GetLastBoard() string { return ui.settings.LastBoard } +func (ui *UIManager) GetPasscode() string { + return ui.settings.Passcode +} + +func (ui *UIManager) GetKey() string { + return ui.settings.Key +} + func (ui *UIManager) ShowAbout() { aboutWindow := ui.app.NewWindow("Об аппке") aboutWindow.Resize(fyne.NewSize(400, 300)) @@ -251,7 +317,7 @@ func (ui *UIManager) ShowAbout() { widget.NewLabel("MobileMkch"), widget.NewLabel("Мобильный клиент для мкача"), widget.NewSeparator(), - widget.NewLabel("Версия: 2.0.0-alpha (Always in alpha lol)"), + widget.NewLabel("Версия: 3.0.0-alpha (Always in alpha lol)"), widget.NewLabel("Автор: w^x (лейн, платон, а похуй как угодно)"), widget.NewLabel("Разработано с ❤️ на Go + Fyne"), ) diff --git a/ui/settings_screen.go b/ui/settings_screen.go index 03f4752..7498090 100644 --- a/ui/settings_screen.go +++ b/ui/settings_screen.go @@ -1,6 +1,7 @@ package ui import ( + "fmt" "fyne.io/fyne/v2" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/widget" @@ -109,6 +110,76 @@ func (ss *SettingsScreen) setupContent() { lastBoardContainer := container.NewHBox(lastBoardLabel, lastBoardValue) ss.content.Add(lastBoardContainer) + ss.content.Add(widget.NewSeparator()) + + passcodeHeader := widget.NewLabel("Passcode") + passcodeHeader.TextStyle = fyne.TextStyle{Bold: true} + ss.content.Add(passcodeHeader) + + passcodeEntry := widget.NewEntry() + passcodeEntry.SetPlaceHolder("Введите passcode для постинга") + passcodeEntry.Text = ss.uiManager.GetSettings().Passcode + passcodeEntry.OnChanged = func(text string) { + ss.uiManager.GetSettings().Passcode = text + ss.uiManager.SaveSettings() + } + ss.content.Add(passcodeEntry) + + keyHeader := widget.NewLabel("Ключ аутентификации") + keyHeader.TextStyle = fyne.TextStyle{Bold: true} + ss.content.Add(keyHeader) + + keyEntry := widget.NewEntry() + keyEntry.SetPlaceHolder("Введите ключ для аутентификации") + keyEntry.Text = ss.uiManager.GetSettings().Key + keyEntry.OnChanged = func(text string) { + ss.uiManager.GetSettings().Key = text + ss.uiManager.SaveSettings() + } + ss.content.Add(keyEntry) + + authButtonsContainer := container.NewHBox() + + testKeyButton := widget.NewButton("Тест ключа", func() { + key := ss.uiManager.GetSettings().Key + if key == "" { + ss.uiManager.ShowError("Ошибка", "Ключ не введен") + return + } + + err := ss.uiManager.GetAPIClient().Authenticate(key) + if err != nil { + ss.uiManager.ShowError("Ошибка аутентификации", fmt.Sprintf("Не удалось аутентифицироваться: %v", err)) + } else { + ss.uiManager.ShowInfo("Успех", "Аутентификация по ключу прошла успешно") + } + }) + + testPasscodeButton := widget.NewButton("Тест passcode", func() { + passcode := ss.uiManager.GetSettings().Passcode + if passcode == "" { + ss.uiManager.ShowError("Ошибка", "Passcode не введен") + return + } + + err := ss.uiManager.GetAPIClient().LoginWithPasscode(passcode) + if err != nil { + ss.uiManager.ShowError("Ошибка passcode", fmt.Sprintf("Не удалось войти с passcode: %v", err)) + } else { + ss.uiManager.ShowInfo("Успех", "Вход с passcode прошел успешно") + } + }) + + authButtonsContainer.Add(testKeyButton) + authButtonsContainer.Add(testPasscodeButton) + ss.content.Add(authButtonsContainer) + + // Debug + debugCheck := widget.NewCheck("Debug режим", func(checked bool) { + ss.uiManager.GetAPIClient().EnableDebug(checked) + }) + ss.content.Add(debugCheck) + ss.content.Add(widget.NewSeparator()) cacheHeader := widget.NewLabel("Управление кэшем") cacheHeader.TextStyle = fyne.TextStyle{Bold: true} @@ -166,6 +237,13 @@ func (ss *SettingsScreen) setupContent() { }) ss.content.Add(aboutButton) + ss.content.Add(widget.NewSeparator()) + + infoButton := widget.NewButton("Я думаю тебя направили сюда)", func() { + ss.uiManager.ShowInfoDialog() + }) + ss.content.Add(infoButton) + ss.content.Refresh() } diff --git a/ui/thread_detail_screen.go b/ui/thread_detail_screen.go index a62f0c8..12a99fa 100644 --- a/ui/thread_detail_screen.go +++ b/ui/thread_detail_screen.go @@ -141,6 +141,22 @@ func (tds *ThreadDetailScreen) displayThreadDetail() { header.Wrapping = fyne.TextWrapWord tds.content.Add(header) + buttonsContainer := container.NewHBox() + + addCommentButton := widget.NewButton("Добавить комментарий", func() { + addCommentScreen := NewAddCommentScreen(tds.uiManager, tds.board.Code, tds.thread.ID) + tds.uiManager.PushScreen(addCommentScreen) + }) + buttonsContainer.Add(addCommentButton) + + refreshButton := widget.NewButton("Обновить", func() { + tds.uiManager.ClearCacheForThread(tds.thread.ID) + tds.loadThreadDetail() + }) + buttonsContainer.Add(refreshButton) + + tds.content.Add(buttonsContainer) + threadContainer := tds.createThreadContainer(tds.threadDetail) tds.content.Add(threadContainer) diff --git a/ui/threads_screen.go b/ui/threads_screen.go index bafb587..38021ab 100644 --- a/ui/threads_screen.go +++ b/ui/threads_screen.go @@ -140,6 +140,16 @@ func (ts *ThreadsScreen) displayThreads() { header.TextStyle = fyne.TextStyle{Bold: true} ts.content.Add(header) + buttonsContainer := container.NewHBox() + + createThreadButton := widget.NewButton("Создать тред", func() { + createThreadScreen := NewCreateThreadScreen(ts.uiManager, ts.board.Code) + ts.uiManager.PushScreen(createThreadScreen) + }) + buttonsContainer.Add(createThreadButton) + + ts.content.Add(buttonsContainer) + if len(ts.threads) == 0 { ts.content.Add(widget.NewLabel("Тредов не найдено")) ts.content.Refresh()