v3 (fckn yeaaah ya zaebalsa)

This commit is contained in:
Lain Iwakura 2025-08-05 23:54:04 +03:00
parent a9d2b95886
commit d82a294c27
No known key found for this signature in database
GPG Key ID: C7C18257F2ADC6F8
9 changed files with 494 additions and 37 deletions

View File

@ -9,7 +9,7 @@
- Просмотр деталей треда и комментариев
- Поддержка изображений и видео
- Темная/светлая тема
- Навигация с кнопкой "Назад" (index lore)
- Навигация с кнопкой "Назад"
- Улучшенный заголовок с статическими кнопками
- Система настроек с сохранением:
- Тема (темная/светлая)
@ -17,7 +17,14 @@
- Автообновление
- Показ файлов
- Компактный режим
- Поддержка Android и iOS (с сохранением настроек (блять нахуй я туда полез))
- **Полная поддержка постинга:**
- Аутентификация по ключу
- Аутентификация по passcode
- Создание тредов
- Добавление комментариев
- Автоматическое обновление после постинга
- Кнопка "Обновить" для ручного обновления
- Поддержка Android и iOS
- **Оптимизации для мобильных устройств:**
- Кэширование данных для быстрой загрузки
- Дебаунсинг UI обновлений
@ -34,6 +41,35 @@
- Очистка кэша деталей тредов
- Очистка всего кэша
## Аутентификация и постинг
### Настройка аутентификации
1. Откройте настройки в приложении
2. Введите ключ аутентификации (если есть)
3. Введите passcode для постинга (если есть)
4. Используйте кнопки "Тест ключа" и "Тест passcode" для проверки
### Создание тредов
1. Перейдите в нужную доску
2. Нажмите "Создать тред"
3. Заполните заголовок и текст
4. Нажмите "Создать тред"
### Добавление комментариев
1. Откройте тред
2. Нажмите "Добавить комментарий"
3. Введите текст комментария
4. Нажмите "Добавить комментарий"
### Автоматическое обновление
- После создания треда автоматически обновляется список тредов
- После добавления комментария автоматически обновляется список комментариев
- Используйте кнопку "Обновить" для принудительного обновления
## Сборка
### Desktop
@ -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` - оптимизации для мобильных устройств

View File

@ -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
}

8
cache/cache.go vendored
View File

@ -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
}

19
main.go
View File

@ -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())

View File

@ -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

View File

@ -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"),
)

View File

@ -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()
}

View File

@ -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)

View File

@ -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()