v3 (fckn yeaaah ya zaebalsa)
This commit is contained in:
parent
a9d2b95886
commit
d82a294c27
70
README.md
70
README.md
@ -9,7 +9,7 @@
|
|||||||
- Просмотр деталей треда и комментариев
|
- Просмотр деталей треда и комментариев
|
||||||
- Поддержка изображений и видео
|
- Поддержка изображений и видео
|
||||||
- Темная/светлая тема
|
- Темная/светлая тема
|
||||||
- Навигация с кнопкой "Назад" (index lore)
|
- Навигация с кнопкой "Назад"
|
||||||
- Улучшенный заголовок с статическими кнопками
|
- Улучшенный заголовок с статическими кнопками
|
||||||
- Система настроек с сохранением:
|
- Система настроек с сохранением:
|
||||||
- Тема (темная/светлая)
|
- Тема (темная/светлая)
|
||||||
@ -17,22 +17,58 @@
|
|||||||
- Автообновление
|
- Автообновление
|
||||||
- Показ файлов
|
- Показ файлов
|
||||||
- Компактный режим
|
- Компактный режим
|
||||||
- Поддержка Android и iOS (с сохранением настроек (блять нахуй я туда полез))
|
- **Полная поддержка постинга:**
|
||||||
|
- Аутентификация по ключу
|
||||||
|
- Аутентификация по passcode
|
||||||
|
- Создание тредов
|
||||||
|
- Добавление комментариев
|
||||||
|
- Автоматическое обновление после постинга
|
||||||
|
- Кнопка "Обновить" для ручного обновления
|
||||||
|
- Поддержка Android и iOS
|
||||||
- **Оптимизации для мобильных устройств:**
|
- **Оптимизации для мобильных устройств:**
|
||||||
- Кэширование данных для быстрой загрузки
|
- Кэширование данных для быстрой загрузки
|
||||||
- Дебаунсинг UI обновлений
|
- Дебаунсинг 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 сборка протестирована и работает!**
|
**✅ iOS И Android сборка протестирована и работает!**
|
||||||
|
|
||||||
|
|
||||||
## Требования
|
## Требования
|
||||||
|
|
||||||
- Go 1.24+
|
- Go 1.24+
|
||||||
@ -109,12 +144,13 @@ open MobileMkch.xcodeproj
|
|||||||
|
|
||||||
- Go 1.24+
|
- Go 1.24+
|
||||||
- Fyne v2.6.2
|
- Fyne v2.6.2
|
||||||
- HTTP клиент для API
|
- HTTP клиент для API с поддержкой сессий
|
||||||
|
- Система кэширования с TTL
|
||||||
|
|
||||||
## Структура
|
## Структура
|
||||||
|
|
||||||
- `main.go` - точка входа
|
- `main.go` - точка входа
|
||||||
- `api/client.go` - HTTP клиент для mkch API
|
- `api/client.go` - HTTP клиент для mkch API с поддержкой аутентификации
|
||||||
- `models/models.go` - структуры данных
|
- `models/models.go` - структуры данных
|
||||||
- `settings/settings.go` - система настроек
|
- `settings/settings.go` - система настроек
|
||||||
- `cache/cache.go` - система кэширования с поддержкой пагинации
|
- `cache/cache.go` - система кэширования с поддержкой пагинации
|
||||||
@ -122,7 +158,9 @@ open MobileMkch.xcodeproj
|
|||||||
- `manager.go` - управление экранами
|
- `manager.go` - управление экранами
|
||||||
- `boards_screen.go` - список досок
|
- `boards_screen.go` - список досок
|
||||||
- `threads_screen.go` - треды доски
|
- `threads_screen.go` - треды доски
|
||||||
- `thread_detail_screen.go` - детали треда
|
- `thread_detail_screen.go` - детали треда с кнопкой обновления
|
||||||
- `settings_screen.go` - экран настроек
|
- `create_thread_screen.go` - создание тредов
|
||||||
|
- `add_comment_screen.go` - добавление комментариев
|
||||||
|
- `settings_screen.go` - экран настроек с тестированием аутентификации
|
||||||
- `optimization.go` - утилиты оптимизации
|
- `optimization.go` - утилиты оптимизации
|
||||||
- `mobile_optimizations.go` - оптимизации для мобильных устройств
|
- `mobile_optimizations.go` - оптимизации для мобильных устройств
|
||||||
252
api/client.go
252
api/client.go
@ -1,10 +1,14 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/http/cookiejar"
|
||||||
|
"net/url"
|
||||||
|
"regexp"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"MobileMkch/cache"
|
"MobileMkch/cache"
|
||||||
@ -20,20 +24,121 @@ type Client struct {
|
|||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
baseURL string
|
baseURL string
|
||||||
debug bool
|
debug bool
|
||||||
|
authKey string
|
||||||
|
passcode string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewClient() *Client {
|
func NewClient() *Client {
|
||||||
|
jar, _ := cookiejar.New(nil)
|
||||||
return &Client{
|
return &Client{
|
||||||
httpClient: &http.Client{
|
httpClient: &http.Client{
|
||||||
Timeout: 30 * time.Second,
|
Timeout: 30 * time.Second,
|
||||||
|
Jar: jar,
|
||||||
},
|
},
|
||||||
baseURL: ApiURL,
|
baseURL: BaseURL,
|
||||||
debug: false,
|
debug: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) EnableDebug(enable bool) {
|
func (c *Client) EnableDebug(enable bool) {
|
||||||
c.debug = enable
|
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) {
|
func (c *Client) makeRequest(url string) ([]byte, error) {
|
||||||
@ -78,7 +183,7 @@ func (c *Client) GetBoards() ([]models.Board, error) {
|
|||||||
return boards, nil
|
return boards, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
url := c.baseURL + "/boards/"
|
url := ApiURL + "/boards/"
|
||||||
body, err := c.makeRequest(url)
|
body, err := c.makeRequest(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("ошибка получения досок: %w", err)
|
return nil, fmt.Errorf("ошибка получения досок: %w", err)
|
||||||
@ -111,7 +216,7 @@ func (c *Client) GetThreads(boardCode string) ([]models.Thread, error) {
|
|||||||
return threads, nil
|
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)
|
body, err := c.makeRequest(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("ошибка получения тредов доски %s: %w", boardCode, err)
|
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)
|
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)
|
cache.GetCache().SetThreads(boardCode, threads)
|
||||||
|
|
||||||
if c.debug {
|
if c.debug {
|
||||||
@ -150,7 +248,7 @@ func (c *Client) GetThread(boardCode string, threadID int) (*models.ThreadDetail
|
|||||||
return thread, nil
|
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)
|
body, err := c.makeRequest(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("ошибка получения треда %d: %w", threadID, err)
|
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
|
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)
|
body, err := c.makeRequest(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("ошибка получения комментариев треда %d: %w", threadID, err)
|
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
|
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
8
cache/cache.go
vendored
@ -164,11 +164,11 @@ func (c *Cache) GetThreadsPage(boardCode string, page int) ([]models.Thread, boo
|
|||||||
|
|
||||||
func (c *Cache) SetThreadDetail(threadID int, thread *models.ThreadDetail) {
|
func (c *Cache) SetThreadDetail(threadID int, thread *models.ThreadDetail) {
|
||||||
data, _ := json.Marshal(thread)
|
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) {
|
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 {
|
if !exists {
|
||||||
return nil, false
|
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) {
|
func (c *Cache) SetComments(threadID int, comments []models.Comment) {
|
||||||
data, _ := json.Marshal(comments)
|
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) {
|
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 {
|
if !exists {
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|||||||
19
main.go
19
main.go
@ -27,6 +27,25 @@ func main() {
|
|||||||
|
|
||||||
apiClient := api.NewClient()
|
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)
|
uiManager := ui.NewUIManager(a, w, apiClient)
|
||||||
|
|
||||||
w.SetContent(uiManager.GetMainContent())
|
w.SetContent(uiManager.GetMainContent())
|
||||||
|
|||||||
@ -18,6 +18,8 @@ type Settings struct {
|
|||||||
ShowFiles bool `json:"show_files"`
|
ShowFiles bool `json:"show_files"`
|
||||||
CompactMode bool `json:"compact_mode"`
|
CompactMode bool `json:"compact_mode"`
|
||||||
PageSize int `json:"page_size"`
|
PageSize int `json:"page_size"`
|
||||||
|
Passcode string `json:"passcode"`
|
||||||
|
Key string `json:"key"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var settingsPath string
|
var settingsPath string
|
||||||
|
|||||||
@ -4,11 +4,13 @@ import (
|
|||||||
"MobileMkch/api"
|
"MobileMkch/api"
|
||||||
"MobileMkch/models"
|
"MobileMkch/models"
|
||||||
"MobileMkch/settings"
|
"MobileMkch/settings"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"fyne.io/fyne/v2"
|
"fyne.io/fyne/v2"
|
||||||
"fyne.io/fyne/v2/container"
|
"fyne.io/fyne/v2/container"
|
||||||
"fyne.io/fyne/v2/theme"
|
"fyne.io/fyne/v2/theme"
|
||||||
"fyne.io/fyne/v2/widget"
|
"fyne.io/fyne/v2/widget"
|
||||||
|
"MobileMkch/cache"
|
||||||
)
|
)
|
||||||
|
|
||||||
type UIManager struct {
|
type UIManager struct {
|
||||||
@ -42,6 +44,7 @@ func NewUIManager(app fyne.App, window fyne.Window, apiClient *api.Client) *UIMa
|
|||||||
AutoRefresh: true,
|
AutoRefresh: true,
|
||||||
ShowFiles: true,
|
ShowFiles: true,
|
||||||
CompactMode: false,
|
CompactMode: false,
|
||||||
|
Key: "",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -186,6 +189,10 @@ func (ui *UIManager) ShowThreadDetail(board *models.Board, thread *models.Thread
|
|||||||
ui.ShowScreen(threadScreen)
|
ui.ShowScreen(threadScreen)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ui *UIManager) PushScreen(screen Screen) {
|
||||||
|
ui.ShowScreen(screen)
|
||||||
|
}
|
||||||
|
|
||||||
func (ui *UIManager) GetAPIClient() *api.Client {
|
func (ui *UIManager) GetAPIClient() *api.Client {
|
||||||
return ui.apiClient
|
return ui.apiClient
|
||||||
}
|
}
|
||||||
@ -195,14 +202,15 @@ func (ui *UIManager) GetWindow() fyne.Window {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ui *UIManager) ShowError(title, message string) {
|
func (ui *UIManager) ShowError(title, message string) {
|
||||||
dialog := widget.NewModalPopUp(
|
var dialog *widget.PopUp
|
||||||
|
dialog = widget.NewModalPopUp(
|
||||||
container.NewVBox(
|
container.NewVBox(
|
||||||
widget.NewLabel(title),
|
widget.NewLabel(title),
|
||||||
widget.NewLabel(""),
|
widget.NewLabel(""),
|
||||||
widget.NewLabel(message),
|
widget.NewLabel(message),
|
||||||
widget.NewLabel(""),
|
widget.NewLabel(""),
|
||||||
widget.NewButton("OK", func() {
|
widget.NewButton("OK", func() {
|
||||||
// Закрыть диалог - пока что просто прячем popup
|
dialog.Hide()
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
ui.window.Canvas(),
|
ui.window.Canvas(),
|
||||||
@ -211,14 +219,15 @@ func (ui *UIManager) ShowError(title, message string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ui *UIManager) ShowInfo(title, message string) {
|
func (ui *UIManager) ShowInfo(title, message string) {
|
||||||
dialog := widget.NewModalPopUp(
|
var dialog *widget.PopUp
|
||||||
|
dialog = widget.NewModalPopUp(
|
||||||
container.NewVBox(
|
container.NewVBox(
|
||||||
widget.NewLabel(title),
|
widget.NewLabel(title),
|
||||||
widget.NewLabel(""),
|
widget.NewLabel(""),
|
||||||
widget.NewLabel(message),
|
widget.NewLabel(message),
|
||||||
widget.NewLabel(""),
|
widget.NewLabel(""),
|
||||||
widget.NewButton("OK", func() {
|
widget.NewButton("OK", func() {
|
||||||
// Закрыть диалог
|
dialog.Hide()
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
ui.window.Canvas(),
|
ui.window.Canvas(),
|
||||||
@ -226,6 +235,32 @@ func (ui *UIManager) ShowInfo(title, message string) {
|
|||||||
dialog.Show()
|
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 {
|
func (ui *UIManager) GetSettings() *settings.Settings {
|
||||||
return ui.settings
|
return ui.settings
|
||||||
}
|
}
|
||||||
@ -234,6 +269,29 @@ func (ui *UIManager) SaveSettings() error {
|
|||||||
return settings.Save(ui.settings)
|
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) {
|
func (ui *UIManager) SetLastBoard(boardCode string) {
|
||||||
ui.settings.LastBoard = boardCode
|
ui.settings.LastBoard = boardCode
|
||||||
ui.SaveSettings()
|
ui.SaveSettings()
|
||||||
@ -243,6 +301,14 @@ func (ui *UIManager) GetLastBoard() string {
|
|||||||
return ui.settings.LastBoard
|
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() {
|
func (ui *UIManager) ShowAbout() {
|
||||||
aboutWindow := ui.app.NewWindow("Об аппке")
|
aboutWindow := ui.app.NewWindow("Об аппке")
|
||||||
aboutWindow.Resize(fyne.NewSize(400, 300))
|
aboutWindow.Resize(fyne.NewSize(400, 300))
|
||||||
@ -251,7 +317,7 @@ func (ui *UIManager) ShowAbout() {
|
|||||||
widget.NewLabel("MobileMkch"),
|
widget.NewLabel("MobileMkch"),
|
||||||
widget.NewLabel("Мобильный клиент для мкача"),
|
widget.NewLabel("Мобильный клиент для мкача"),
|
||||||
widget.NewSeparator(),
|
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("Автор: w^x (лейн, платон, а похуй как угодно)"),
|
||||||
widget.NewLabel("Разработано с ❤️ на Go + Fyne"),
|
widget.NewLabel("Разработано с ❤️ на Go + Fyne"),
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package ui
|
package ui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"fyne.io/fyne/v2"
|
"fyne.io/fyne/v2"
|
||||||
"fyne.io/fyne/v2/container"
|
"fyne.io/fyne/v2/container"
|
||||||
"fyne.io/fyne/v2/widget"
|
"fyne.io/fyne/v2/widget"
|
||||||
@ -109,6 +110,76 @@ func (ss *SettingsScreen) setupContent() {
|
|||||||
lastBoardContainer := container.NewHBox(lastBoardLabel, lastBoardValue)
|
lastBoardContainer := container.NewHBox(lastBoardLabel, lastBoardValue)
|
||||||
ss.content.Add(lastBoardContainer)
|
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())
|
ss.content.Add(widget.NewSeparator())
|
||||||
cacheHeader := widget.NewLabel("Управление кэшем")
|
cacheHeader := widget.NewLabel("Управление кэшем")
|
||||||
cacheHeader.TextStyle = fyne.TextStyle{Bold: true}
|
cacheHeader.TextStyle = fyne.TextStyle{Bold: true}
|
||||||
@ -166,6 +237,13 @@ func (ss *SettingsScreen) setupContent() {
|
|||||||
})
|
})
|
||||||
ss.content.Add(aboutButton)
|
ss.content.Add(aboutButton)
|
||||||
|
|
||||||
|
ss.content.Add(widget.NewSeparator())
|
||||||
|
|
||||||
|
infoButton := widget.NewButton("Я думаю тебя направили сюда)", func() {
|
||||||
|
ss.uiManager.ShowInfoDialog()
|
||||||
|
})
|
||||||
|
ss.content.Add(infoButton)
|
||||||
|
|
||||||
ss.content.Refresh()
|
ss.content.Refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -141,6 +141,22 @@ func (tds *ThreadDetailScreen) displayThreadDetail() {
|
|||||||
header.Wrapping = fyne.TextWrapWord
|
header.Wrapping = fyne.TextWrapWord
|
||||||
tds.content.Add(header)
|
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)
|
threadContainer := tds.createThreadContainer(tds.threadDetail)
|
||||||
tds.content.Add(threadContainer)
|
tds.content.Add(threadContainer)
|
||||||
|
|
||||||
|
|||||||
@ -140,6 +140,16 @@ func (ts *ThreadsScreen) displayThreads() {
|
|||||||
header.TextStyle = fyne.TextStyle{Bold: true}
|
header.TextStyle = fyne.TextStyle{Bold: true}
|
||||||
ts.content.Add(header)
|
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 {
|
if len(ts.threads) == 0 {
|
||||||
ts.content.Add(widget.NewLabel("Тредов не найдено"))
|
ts.content.Add(widget.NewLabel("Тредов не найдено"))
|
||||||
ts.content.Refresh()
|
ts.content.Refresh()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user