v2 (fckn yeaaah)
This commit is contained in:
parent
c9fcd918ce
commit
a9d2b95886
41
README.md
41
README.md
@ -4,12 +4,35 @@
|
|||||||
|
|
||||||
## Возможности
|
## Возможности
|
||||||
|
|
||||||
- Просмотр досок и тредов
|
- Просмотр всех досок Мкача
|
||||||
- Чтение комментариев
|
- Просмотр тредов в каждой доске
|
||||||
- Темная/светлая тема
|
- Просмотр деталей треда и комментариев
|
||||||
- Навигация между экранами
|
|
||||||
- Поддержка изображений и видео
|
- Поддержка изображений и видео
|
||||||
- Адаптивный интерфейс
|
- Темная/светлая тема
|
||||||
|
- Навигация с кнопкой "Назад" (index lore)
|
||||||
|
- Улучшенный заголовок с статическими кнопками
|
||||||
|
- Система настроек с сохранением:
|
||||||
|
- Тема (темная/светлая)
|
||||||
|
- Последняя посещенная доска
|
||||||
|
- Автообновление
|
||||||
|
- Показ файлов
|
||||||
|
- Компактный режим
|
||||||
|
- Поддержка Android и iOS (с сохранением настроек (блять нахуй я туда полез))
|
||||||
|
- **Оптимизации для мобильных устройств:**
|
||||||
|
- Кэширование данных для быстрой загрузки
|
||||||
|
- Дебаунсинг UI обновлений
|
||||||
|
- Оптимизация потребления батареи
|
||||||
|
- Агрессивная сборка мусора на мобильных
|
||||||
|
- Ограничение частоты сетевых запросов
|
||||||
|
- **Многостраничность для списка тредов:**
|
||||||
|
- Настраиваемый размер страницы (5-20 тредов)
|
||||||
|
- Навигация между страницами
|
||||||
|
- Ограничение тредов для больших досок
|
||||||
|
- **Управление кэшем:**
|
||||||
|
- Очистка кэша досок
|
||||||
|
- Очистка кэша тредов для всех досок
|
||||||
|
- Очистка кэша деталей тредов
|
||||||
|
- Очистка всего кэша
|
||||||
|
|
||||||
## Сборка
|
## Сборка
|
||||||
|
|
||||||
@ -72,7 +95,8 @@ open MobileMkch.xcodeproj
|
|||||||
|
|
||||||
5. Подпишите и установите через Xcode или TestFlight
|
5. Подпишите и установите через Xcode или TestFlight
|
||||||
|
|
||||||
**✅ iOS сборка протестирована и работает!**
|
**✅ iOS И Android сборка протестирована и работает!**
|
||||||
|
|
||||||
|
|
||||||
## Требования
|
## Требования
|
||||||
|
|
||||||
@ -92,8 +116,13 @@ open MobileMkch.xcodeproj
|
|||||||
- `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` - система настроек
|
||||||
|
- `cache/cache.go` - система кэширования с поддержкой пагинации
|
||||||
- `ui/` - пользовательский интерфейс
|
- `ui/` - пользовательский интерфейс
|
||||||
- `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` - экран настроек
|
||||||
|
- `optimization.go` - утилиты оптимизации
|
||||||
|
- `mobile_optimizations.go` - оптимизации для мобильных устройств
|
||||||
@ -7,6 +7,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"MobileMkch/cache"
|
||||||
"MobileMkch/models"
|
"MobileMkch/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -69,6 +70,14 @@ func (c *Client) makeRequest(url string) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) GetBoards() ([]models.Board, error) {
|
func (c *Client) GetBoards() ([]models.Board, error) {
|
||||||
|
// Поздравляю, кеш
|
||||||
|
if boards, exists := cache.GetCache().GetBoards(); exists {
|
||||||
|
if c.debug {
|
||||||
|
fmt.Printf("[API] Доски загружены из кэша\n")
|
||||||
|
}
|
||||||
|
return boards, nil
|
||||||
|
}
|
||||||
|
|
||||||
url := c.baseURL + "/boards/"
|
url := c.baseURL + "/boards/"
|
||||||
body, err := c.makeRequest(url)
|
body, err := c.makeRequest(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -84,6 +93,9 @@ func (c *Client) GetBoards() ([]models.Board, error) {
|
|||||||
return nil, fmt.Errorf("ошибка парсинга досок: %w", err)
|
return nil, fmt.Errorf("ошибка парсинга досок: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Всех победили
|
||||||
|
cache.GetCache().SetBoards(boards)
|
||||||
|
|
||||||
if c.debug {
|
if c.debug {
|
||||||
fmt.Printf("[API] Получено досок: %d\n", len(boards))
|
fmt.Printf("[API] Получено досок: %d\n", len(boards))
|
||||||
}
|
}
|
||||||
@ -92,6 +104,13 @@ func (c *Client) GetBoards() ([]models.Board, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) GetThreads(boardCode string) ([]models.Thread, error) {
|
func (c *Client) GetThreads(boardCode string) ([]models.Thread, error) {
|
||||||
|
if threads, exists := cache.GetCache().GetThreads(boardCode); exists {
|
||||||
|
if c.debug {
|
||||||
|
fmt.Printf("[API] Треды загружены из кэша для /%s/\n", boardCode)
|
||||||
|
}
|
||||||
|
return threads, nil
|
||||||
|
}
|
||||||
|
|
||||||
url := fmt.Sprintf("%s/board/%s", c.baseURL, boardCode)
|
url := fmt.Sprintf("%s/board/%s", c.baseURL, boardCode)
|
||||||
body, err := c.makeRequest(url)
|
body, err := c.makeRequest(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -107,6 +126,15 @@ 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)
|
||||||
|
|
||||||
if c.debug {
|
if c.debug {
|
||||||
fmt.Printf("[API] Получено тредов в /%s/: %d\n", boardCode, len(threads))
|
fmt.Printf("[API] Получено тредов в /%s/: %d\n", boardCode, len(threads))
|
||||||
}
|
}
|
||||||
@ -115,6 +143,13 @@ func (c *Client) GetThreads(boardCode string) ([]models.Thread, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) GetThread(boardCode string, threadID int) (*models.ThreadDetail, error) {
|
func (c *Client) GetThread(boardCode string, threadID int) (*models.ThreadDetail, error) {
|
||||||
|
if thread, exists := cache.GetCache().GetThreadDetail(threadID); exists {
|
||||||
|
if c.debug {
|
||||||
|
fmt.Printf("[API] Тред %d загружен из кэша\n", threadID)
|
||||||
|
}
|
||||||
|
return thread, nil
|
||||||
|
}
|
||||||
|
|
||||||
url := fmt.Sprintf("%s/board/%s/thread/%d", c.baseURL, boardCode, threadID)
|
url := fmt.Sprintf("%s/board/%s/thread/%d", c.baseURL, boardCode, threadID)
|
||||||
body, err := c.makeRequest(url)
|
body, err := c.makeRequest(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -130,6 +165,8 @@ func (c *Client) GetThread(boardCode string, threadID int) (*models.ThreadDetail
|
|||||||
return nil, fmt.Errorf("ошибка парсинга треда: %w", err)
|
return nil, fmt.Errorf("ошибка парсинга треда: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cache.GetCache().SetThreadDetail(threadID, &thread)
|
||||||
|
|
||||||
if c.debug {
|
if c.debug {
|
||||||
fmt.Printf("[API] Получен тред: ID=%d, Title=%s\n", thread.ID, thread.Title)
|
fmt.Printf("[API] Получен тред: ID=%d, Title=%s\n", thread.ID, thread.Title)
|
||||||
}
|
}
|
||||||
@ -138,6 +175,13 @@ func (c *Client) GetThread(boardCode string, threadID int) (*models.ThreadDetail
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) GetComments(boardCode string, threadID int) ([]models.Comment, error) {
|
func (c *Client) GetComments(boardCode string, threadID int) ([]models.Comment, error) {
|
||||||
|
if comments, exists := cache.GetCache().GetComments(threadID); exists {
|
||||||
|
if c.debug {
|
||||||
|
fmt.Printf("[API] Комментарии загружены из кэша для треда %d\n", threadID)
|
||||||
|
}
|
||||||
|
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", c.baseURL, boardCode, threadID)
|
||||||
body, err := c.makeRequest(url)
|
body, err := c.makeRequest(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -153,6 +197,8 @@ func (c *Client) GetComments(boardCode string, threadID int) ([]models.Comment,
|
|||||||
return nil, fmt.Errorf("ошибка парсинга комментариев: %w", err)
|
return nil, fmt.Errorf("ошибка парсинга комментариев: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cache.GetCache().SetComments(threadID, comments)
|
||||||
|
|
||||||
if c.debug {
|
if c.debug {
|
||||||
fmt.Printf("[API] Получено комментариев: %d\n", len(comments))
|
fmt.Printf("[API] Получено комментариев: %d\n", len(comments))
|
||||||
}
|
}
|
||||||
|
|||||||
203
cache/cache.go
vendored
Normal file
203
cache/cache.go
vendored
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
package cache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"MobileMkch/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Cache struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
items map[string]cacheItem
|
||||||
|
}
|
||||||
|
|
||||||
|
type cacheItem struct {
|
||||||
|
Data interface{}
|
||||||
|
Timestamp time.Time
|
||||||
|
TTL time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
instance *Cache
|
||||||
|
once sync.Once
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetCache() *Cache {
|
||||||
|
once.Do(func() {
|
||||||
|
instance = &Cache{
|
||||||
|
items: make(map[string]cacheItem),
|
||||||
|
}
|
||||||
|
go instance.cleanup()
|
||||||
|
})
|
||||||
|
return instance
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache) Set(key string, data interface{}, ttl time.Duration) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
c.items[key] = cacheItem{
|
||||||
|
Data: data,
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
TTL: ttl,
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Кэшировано: %s (TTL: %v)", key, ttl)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache) Get(key string) (interface{}, bool) {
|
||||||
|
c.mu.RLock()
|
||||||
|
defer c.mu.RUnlock()
|
||||||
|
|
||||||
|
item, exists := c.items[key]
|
||||||
|
if !exists {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
if time.Since(item.Timestamp) > item.TTL {
|
||||||
|
log.Printf("Кэш устарел: %s", key)
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Кэш найден: %s", key)
|
||||||
|
return item.Data, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache) Delete(key string) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
delete(c.items, key)
|
||||||
|
log.Printf("Кэш удален: %s", key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache) Clear() {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
c.items = make(map[string]cacheItem)
|
||||||
|
log.Printf("Кэш очищен")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache) cleanup() {
|
||||||
|
ticker := time.NewTicker(5 * time.Minute)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for range ticker.C {
|
||||||
|
c.mu.Lock()
|
||||||
|
now := time.Now()
|
||||||
|
for key, item := range c.items {
|
||||||
|
if now.Sub(item.Timestamp) > item.TTL {
|
||||||
|
delete(c.items, key)
|
||||||
|
log.Printf("Автоочистка кэша: %s", key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.mu.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache) SetBoards(boards []models.Board) {
|
||||||
|
data, _ := json.Marshal(boards)
|
||||||
|
c.Set("boards", data, 10*time.Minute)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache) GetBoards() ([]models.Board, bool) {
|
||||||
|
data, exists := c.Get("boards")
|
||||||
|
if !exists {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
var boards []models.Board
|
||||||
|
if err := json.Unmarshal(data.([]byte), &boards); err != nil {
|
||||||
|
log.Printf("Ошибка десериализации досок: %v", err)
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return boards, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache) SetThreads(boardCode string, threads []models.Thread) {
|
||||||
|
data, _ := json.Marshal(threads)
|
||||||
|
c.Set("threads_"+boardCode, data, 5*time.Minute)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache) GetThreads(boardCode string) ([]models.Thread, bool) {
|
||||||
|
data, exists := c.Get("threads_" + boardCode)
|
||||||
|
if !exists {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
var threads []models.Thread
|
||||||
|
if err := json.Unmarshal(data.([]byte), &threads); err != nil {
|
||||||
|
log.Printf("Ошибка десериализации тредов: %v", err)
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return threads, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache) SetThreadsPage(boardCode string, page int, threads []models.Thread) {
|
||||||
|
data, _ := json.Marshal(threads)
|
||||||
|
key := fmt.Sprintf("threads_%s_page_%d", boardCode, page)
|
||||||
|
c.Set(key, data, 3*time.Minute)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache) GetThreadsPage(boardCode string, page int) ([]models.Thread, bool) {
|
||||||
|
key := fmt.Sprintf("threads_%s_page_%d", boardCode, page)
|
||||||
|
data, exists := c.Get(key)
|
||||||
|
if !exists {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
var threads []models.Thread
|
||||||
|
if err := json.Unmarshal(data.([]byte), &threads); err != nil {
|
||||||
|
log.Printf("Ошибка десериализации тредов страницы: %v", err)
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return threads, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache) SetThreadDetail(threadID int, thread *models.ThreadDetail) {
|
||||||
|
data, _ := json.Marshal(thread)
|
||||||
|
c.Set("thread_"+string(rune(threadID)), data, 3*time.Minute)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache) GetThreadDetail(threadID int) (*models.ThreadDetail, bool) {
|
||||||
|
data, exists := c.Get("thread_" + string(rune(threadID)))
|
||||||
|
if !exists {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
var thread models.ThreadDetail
|
||||||
|
if err := json.Unmarshal(data.([]byte), &thread); err != nil {
|
||||||
|
log.Printf("Ошибка десериализации треда: %v", err)
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return &thread, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache) SetComments(threadID int, comments []models.Comment) {
|
||||||
|
data, _ := json.Marshal(comments)
|
||||||
|
c.Set("comments_"+string(rune(threadID)), data, 3*time.Minute)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache) GetComments(threadID int) ([]models.Comment, bool) {
|
||||||
|
data, exists := c.Get("comments_" + string(rune(threadID)))
|
||||||
|
if !exists {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
var comments []models.Comment
|
||||||
|
if err := json.Unmarshal(data.([]byte), &comments); err != nil {
|
||||||
|
log.Printf("Ошибка десериализации комментариев: %v", err)
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return comments, true
|
||||||
|
}
|
||||||
5
main.go
5
main.go
@ -10,6 +10,7 @@ import (
|
|||||||
"fyne.io/fyne/v2/widget"
|
"fyne.io/fyne/v2/widget"
|
||||||
|
|
||||||
"MobileMkch/api"
|
"MobileMkch/api"
|
||||||
|
"MobileMkch/settings"
|
||||||
"MobileMkch/ui"
|
"MobileMkch/ui"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -17,7 +18,9 @@ func main() {
|
|||||||
a := app.NewWithID("com.mkch.mobile")
|
a := app.NewWithID("com.mkch.mobile")
|
||||||
a.SetIcon(theme.ComputerIcon())
|
a.SetIcon(theme.ComputerIcon())
|
||||||
|
|
||||||
a.Settings().SetTheme(theme.DarkTheme())
|
settings.SetApp(a)
|
||||||
|
|
||||||
|
MobileOptimizations(a)
|
||||||
|
|
||||||
w := a.NewWindow("MobileMkch")
|
w := a.NewWindow("MobileMkch")
|
||||||
w.Resize(fyne.NewSize(400, 700))
|
w.Resize(fyne.NewSize(400, 700))
|
||||||
|
|||||||
122
mobile_optimizations.go
Normal file
122
mobile_optimizations.go
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"runtime"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"fyne.io/fyne/v2"
|
||||||
|
"fyne.io/fyne/v2/theme"
|
||||||
|
)
|
||||||
|
|
||||||
|
func MobileOptimizations(a fyne.App) {
|
||||||
|
if runtime.GOOS == "android" || runtime.GOOS == "ios" {
|
||||||
|
settings := a.Settings()
|
||||||
|
settings.SetTheme(theme.DarkTheme())
|
||||||
|
|
||||||
|
if runtime.GOOS == "android" {
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
if runtime.GOOS == "ios" {
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type PerformanceMonitor struct {
|
||||||
|
startTime time.Time
|
||||||
|
metrics map[string]time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPerformanceMonitor() *PerformanceMonitor {
|
||||||
|
return &PerformanceMonitor{
|
||||||
|
startTime: time.Now(),
|
||||||
|
metrics: make(map[string]time.Duration),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pm *PerformanceMonitor) StartTimer(name string) {
|
||||||
|
pm.startTime = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pm *PerformanceMonitor) EndTimer(name string) {
|
||||||
|
duration := time.Since(pm.startTime)
|
||||||
|
pm.metrics[name] = duration
|
||||||
|
|
||||||
|
if duration > 100*time.Millisecond {
|
||||||
|
println("Медленная операция:", name, duration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pm *PerformanceMonitor) GetMetrics() map[string]time.Duration {
|
||||||
|
return pm.metrics
|
||||||
|
}
|
||||||
|
|
||||||
|
type MemoryOptimizer struct {
|
||||||
|
lastGC time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMemoryOptimizer() *MemoryOptimizer {
|
||||||
|
return &MemoryOptimizer{
|
||||||
|
lastGC: time.Now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mo *MemoryOptimizer) CheckMemory() {
|
||||||
|
if time.Since(mo.lastGC) > 30*time.Second {
|
||||||
|
if runtime.GOOS == "android" || runtime.GOOS == "ios" {
|
||||||
|
runtime.GC()
|
||||||
|
mo.lastGC = time.Now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type BatteryOptimizer struct {
|
||||||
|
lastUpdate time.Time
|
||||||
|
updateInterval time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBatteryOptimizer() *BatteryOptimizer {
|
||||||
|
interval := 5 * time.Second
|
||||||
|
if runtime.GOOS == "android" || runtime.GOOS == "ios" {
|
||||||
|
interval = 10 * time.Second
|
||||||
|
}
|
||||||
|
|
||||||
|
return &BatteryOptimizer{
|
||||||
|
lastUpdate: time.Now(),
|
||||||
|
updateInterval: interval,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bo *BatteryOptimizer) ShouldUpdate() bool {
|
||||||
|
return time.Since(bo.lastUpdate) >= bo.updateInterval
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bo *BatteryOptimizer) UpdateComplete() {
|
||||||
|
bo.lastUpdate = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
type NetworkOptimizer struct {
|
||||||
|
lastRequest time.Time
|
||||||
|
minInterval time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNetworkOptimizer() *NetworkOptimizer {
|
||||||
|
interval := 1 * time.Second
|
||||||
|
if runtime.GOOS == "android" || runtime.GOOS == "ios" {
|
||||||
|
interval = 2 * time.Second
|
||||||
|
}
|
||||||
|
|
||||||
|
return &NetworkOptimizer{
|
||||||
|
lastRequest: time.Now(),
|
||||||
|
minInterval: interval,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (no *NetworkOptimizer) CanMakeRequest() bool {
|
||||||
|
return time.Since(no.lastRequest) >= no.minInterval
|
||||||
|
}
|
||||||
|
|
||||||
|
func (no *NetworkOptimizer) RequestComplete() {
|
||||||
|
no.lastRequest = time.Now()
|
||||||
|
}
|
||||||
@ -15,7 +15,6 @@ type Thread struct {
|
|||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Text string `json:"text"`
|
Text string `json:"text"`
|
||||||
Creation string `json:"creation"`
|
Creation string `json:"creation"`
|
||||||
Author string `json:"author"`
|
|
||||||
Board string `json:"board"`
|
Board string `json:"board"`
|
||||||
Rating *int `json:"rating,omitempty"`
|
Rating *int `json:"rating,omitempty"`
|
||||||
Pinned *bool `json:"pinned,omitempty"`
|
Pinned *bool `json:"pinned,omitempty"`
|
||||||
@ -45,7 +44,6 @@ type ThreadDetail struct {
|
|||||||
Creation string `json:"creation"`
|
Creation string `json:"creation"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Text string `json:"text"`
|
Text string `json:"text"`
|
||||||
Author string `json:"author"`
|
|
||||||
Board string `json:"board"`
|
Board string `json:"board"`
|
||||||
Files []string `json:"files"`
|
Files []string `json:"files"`
|
||||||
}
|
}
|
||||||
|
|||||||
159
settings/settings.go
Normal file
159
settings/settings.go
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
package settings
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"fyne.io/fyne/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Settings struct {
|
||||||
|
Theme string `json:"theme"`
|
||||||
|
LastBoard string `json:"last_board"`
|
||||||
|
AutoRefresh bool `json:"auto_refresh"`
|
||||||
|
ShowFiles bool `json:"show_files"`
|
||||||
|
CompactMode bool `json:"compact_mode"`
|
||||||
|
PageSize int `json:"page_size"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var settingsPath string
|
||||||
|
var fyneApp fyne.App
|
||||||
|
|
||||||
|
func SetApp(app fyne.App) {
|
||||||
|
fyneApp = app
|
||||||
|
}
|
||||||
|
|
||||||
|
func Init() error {
|
||||||
|
var appDir string
|
||||||
|
|
||||||
|
if runtime.GOOS == "android" || runtime.GOOS == "ios" {
|
||||||
|
appDir = filepath.Join(os.TempDir(), "mobilemkch")
|
||||||
|
log.Printf("Используем временную директорию для %s: %s", runtime.GOOS, appDir)
|
||||||
|
} else {
|
||||||
|
homeDir, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
appDir = filepath.Join(homeDir, ".mobilemkch")
|
||||||
|
log.Printf("Используем домашнюю директорию: %s", appDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(appDir, 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settingsPath = filepath.Join(appDir, "settings.json")
|
||||||
|
log.Printf("Путь к настройкам: %s", settingsPath)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Load() (*Settings, error) {
|
||||||
|
settings := &Settings{
|
||||||
|
Theme: "dark",
|
||||||
|
LastBoard: "",
|
||||||
|
AutoRefresh: true,
|
||||||
|
ShowFiles: true,
|
||||||
|
CompactMode: false,
|
||||||
|
PageSize: 10,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (runtime.GOOS == "android" || runtime.GOOS == "ios") && fyneApp != nil {
|
||||||
|
log.Printf("Загружаем настройки через Fyne Storage")
|
||||||
|
storage := fyneApp.Storage()
|
||||||
|
|
||||||
|
reader, err := storage.Open("settings.json")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Файл настроек не найден в Fyne Storage, создаем новый")
|
||||||
|
return settings, Save(settings)
|
||||||
|
}
|
||||||
|
defer reader.Close()
|
||||||
|
|
||||||
|
data, err := io.ReadAll(reader)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Ошибка чтения из Fyne Storage: %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(data, settings); err != nil {
|
||||||
|
log.Printf("Ошибка парсинга настроек из Fyne Storage: %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Настройки загружены из Fyne Storage")
|
||||||
|
return settings, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if settingsPath == "" {
|
||||||
|
if err := Init(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Загружаем настройки из: %s", settingsPath)
|
||||||
|
data, err := os.ReadFile(settingsPath)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
log.Printf("Файл настроек не найден, создаем новый")
|
||||||
|
return settings, Save(settings)
|
||||||
|
}
|
||||||
|
log.Printf("Ошибка чтения настроек: %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(data, settings); err != nil {
|
||||||
|
log.Printf("Ошибка парсинга настроек: %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Настройки загружены успешно")
|
||||||
|
return settings, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Save(settings *Settings) error {
|
||||||
|
if (runtime.GOOS == "android" || runtime.GOOS == "ios") && fyneApp != nil {
|
||||||
|
log.Printf("Сохраняем настройки через Fyne Storage")
|
||||||
|
storage := fyneApp.Storage()
|
||||||
|
|
||||||
|
data, err := json.MarshalIndent(settings, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Ошибка сериализации настроек: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
writer, err := storage.Create("settings.json")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Ошибка создания файла в Fyne Storage: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer writer.Close()
|
||||||
|
|
||||||
|
_, err = writer.Write(data)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Ошибка сохранения через Fyne Storage: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Настройки сохранены через Fyne Storage")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Сохраняем настройки в: %s", settingsPath)
|
||||||
|
data, err := json.MarshalIndent(settings, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Ошибка сериализации настроек: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.WriteFile(settingsPath, data, 0644)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Ошибка записи настроек: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Настройки сохранены успешно")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@ -150,6 +150,7 @@ func (bs *BoardsScreen) createBoardCard(board *models.Board) fyne.CanvasObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
button := widget.NewButton(fmt.Sprintf("%s\n%s", title, description), func() {
|
button := widget.NewButton(fmt.Sprintf("%s\n%s", title, description), func() {
|
||||||
|
bs.uiManager.SetLastBoard(board.Code)
|
||||||
bs.uiManager.ShowBoardThreads(board)
|
bs.uiManager.ShowBoardThreads(board)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
108
ui/manager.go
108
ui/manager.go
@ -1,13 +1,14 @@
|
|||||||
package ui
|
package ui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"MobileMkch/api"
|
||||||
|
"MobileMkch/models"
|
||||||
|
"MobileMkch/settings"
|
||||||
|
|
||||||
"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/api"
|
|
||||||
"MobileMkch/models"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type UIManager struct {
|
type UIManager struct {
|
||||||
@ -22,7 +23,7 @@ type UIManager struct {
|
|||||||
headerBar *fyne.Container
|
headerBar *fyne.Container
|
||||||
content *fyne.Container
|
content *fyne.Container
|
||||||
|
|
||||||
isDarkTheme bool
|
settings *settings.Settings
|
||||||
}
|
}
|
||||||
|
|
||||||
type Screen interface {
|
type Screen interface {
|
||||||
@ -33,13 +34,30 @@ type Screen interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewUIManager(app fyne.App, window fyne.Window, apiClient *api.Client) *UIManager {
|
func NewUIManager(app fyne.App, window fyne.Window, apiClient *api.Client) *UIManager {
|
||||||
|
appSettings, err := settings.Load()
|
||||||
|
if err != nil {
|
||||||
|
appSettings = &settings.Settings{
|
||||||
|
Theme: "dark",
|
||||||
|
LastBoard: "",
|
||||||
|
AutoRefresh: true,
|
||||||
|
ShowFiles: true,
|
||||||
|
CompactMode: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
manager := &UIManager{
|
manager := &UIManager{
|
||||||
app: app,
|
app: app,
|
||||||
window: window,
|
window: window,
|
||||||
apiClient: apiClient,
|
apiClient: apiClient,
|
||||||
navigationStack: make([]Screen, 0),
|
navigationStack: make([]Screen, 0),
|
||||||
content: container.NewMax(),
|
content: container.NewMax(),
|
||||||
isDarkTheme: true, // По умолчанию темная тема
|
settings: appSettings,
|
||||||
|
}
|
||||||
|
|
||||||
|
if appSettings.Theme == "dark" {
|
||||||
|
app.Settings().SetTheme(theme.DarkTheme())
|
||||||
|
} else {
|
||||||
|
app.Settings().SetTheme(theme.LightTheme())
|
||||||
}
|
}
|
||||||
|
|
||||||
manager.setupHeader()
|
manager.setupHeader()
|
||||||
@ -52,30 +70,30 @@ func NewUIManager(app fyne.App, window fyne.Window, apiClient *api.Client) *UIMa
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ui *UIManager) setupHeader() {
|
func (ui *UIManager) setupHeader() {
|
||||||
titleLabel := widget.NewLabel("MobileMkch")
|
backButton := widget.NewButton("<- Назад", func() {
|
||||||
titleLabel.TextStyle = fyne.TextStyle{Bold: true}
|
|
||||||
|
|
||||||
backButton := widget.NewButton("← Назад", func() {
|
|
||||||
ui.GoBack()
|
ui.GoBack()
|
||||||
})
|
})
|
||||||
backButton.Hide()
|
backButton.Hide()
|
||||||
|
|
||||||
|
titleLabel := widget.NewLabel("MobileMkch")
|
||||||
|
titleLabel.TextStyle = fyne.TextStyle{Bold: true}
|
||||||
|
titleLabel.Alignment = fyne.TextAlignCenter
|
||||||
|
|
||||||
refreshButton := widget.NewButton("🔄", func() {
|
refreshButton := widget.NewButton("🔄", func() {
|
||||||
if ui.currentScreen != nil {
|
if ui.currentScreen != nil {
|
||||||
ui.currentScreen.OnShow()
|
ui.currentScreen.OnShow()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
themeButton := widget.NewButton("🌙", func() {
|
settingsButton := widget.NewButton("⚙️", func() {
|
||||||
ui.toggleTheme()
|
settingsScreen := NewSettingsScreen(ui)
|
||||||
|
ui.ShowScreen(settingsScreen)
|
||||||
})
|
})
|
||||||
|
|
||||||
rightButtons := container.NewHBox(refreshButton, themeButton)
|
rightButtons := container.NewHBox(refreshButton, settingsButton)
|
||||||
|
|
||||||
ui.headerBar = container.NewHBox(
|
ui.headerBar = container.NewBorder(
|
||||||
backButton,
|
nil, nil, backButton, rightButtons, titleLabel,
|
||||||
titleLabel,
|
|
||||||
rightButtons,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,12 +118,10 @@ func (ui *UIManager) ShowScreen(screen Screen) {
|
|||||||
|
|
||||||
ui.navigationStack = append(ui.navigationStack, screen)
|
ui.navigationStack = append(ui.navigationStack, screen)
|
||||||
ui.currentScreen = screen
|
ui.currentScreen = screen
|
||||||
|
|
||||||
ui.content.RemoveAll()
|
ui.content.RemoveAll()
|
||||||
ui.content.Add(screen.GetContent())
|
ui.content.Add(screen.GetContent())
|
||||||
ui.content.Refresh()
|
|
||||||
|
|
||||||
ui.updateHeader(screen.GetTitle())
|
ui.updateHeader(screen.GetTitle())
|
||||||
|
ui.content.Refresh()
|
||||||
|
|
||||||
screen.OnShow()
|
screen.OnShow()
|
||||||
}
|
}
|
||||||
@ -132,22 +148,23 @@ func (ui *UIManager) GoBack() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ui *UIManager) toggleTheme() {
|
func (ui *UIManager) toggleTheme() {
|
||||||
ui.isDarkTheme = !ui.isDarkTheme
|
if ui.settings.Theme == "dark" {
|
||||||
|
ui.settings.Theme = "light"
|
||||||
if ui.isDarkTheme {
|
|
||||||
ui.app.Settings().SetTheme(theme.DarkTheme())
|
|
||||||
} else {
|
|
||||||
ui.app.Settings().SetTheme(theme.LightTheme())
|
ui.app.Settings().SetTheme(theme.LightTheme())
|
||||||
|
} else {
|
||||||
|
ui.settings.Theme = "dark"
|
||||||
|
ui.app.Settings().SetTheme(theme.DarkTheme())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
settings.Save(ui.settings)
|
||||||
ui.window.Canvas().Refresh(ui.window.Canvas().Content())
|
ui.window.Canvas().Refresh(ui.window.Canvas().Content())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ui *UIManager) updateHeader(title string) {
|
func (ui *UIManager) updateHeader(title string) {
|
||||||
ui.window.SetTitle(title)
|
ui.window.SetTitle(title)
|
||||||
|
|
||||||
if len(ui.headerBar.Objects) > 0 {
|
if len(ui.headerBar.Objects) >= 3 {
|
||||||
if backBtn, ok := ui.headerBar.Objects[0].(*widget.Button); ok {
|
if backBtn, ok := ui.headerBar.Objects[1].(*widget.Button); ok {
|
||||||
if len(ui.navigationStack) > 1 {
|
if len(ui.navigationStack) > 1 {
|
||||||
backBtn.Show()
|
backBtn.Show()
|
||||||
} else {
|
} else {
|
||||||
@ -208,3 +225,42 @@ func (ui *UIManager) ShowInfo(title, message string) {
|
|||||||
)
|
)
|
||||||
dialog.Show()
|
dialog.Show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ui *UIManager) GetSettings() *settings.Settings {
|
||||||
|
return ui.settings
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *UIManager) SaveSettings() error {
|
||||||
|
return settings.Save(ui.settings)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *UIManager) SetLastBoard(boardCode string) {
|
||||||
|
ui.settings.LastBoard = boardCode
|
||||||
|
ui.SaveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *UIManager) GetLastBoard() string {
|
||||||
|
return ui.settings.LastBoard
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *UIManager) ShowAbout() {
|
||||||
|
aboutWindow := ui.app.NewWindow("Об аппке")
|
||||||
|
aboutWindow.Resize(fyne.NewSize(400, 300))
|
||||||
|
|
||||||
|
content := container.NewVBox(
|
||||||
|
widget.NewLabel("MobileMkch"),
|
||||||
|
widget.NewLabel("Мобильный клиент для мкача"),
|
||||||
|
widget.NewSeparator(),
|
||||||
|
widget.NewLabel("Версия: 2.0.0-alpha (Always in alpha lol)"),
|
||||||
|
widget.NewLabel("Автор: w^x (лейн, платон, а похуй как угодно)"),
|
||||||
|
widget.NewLabel("Разработано с ❤️ на Go + Fyne"),
|
||||||
|
)
|
||||||
|
|
||||||
|
closeButton := widget.NewButton("Закрыть", func() {
|
||||||
|
aboutWindow.Close()
|
||||||
|
})
|
||||||
|
content.Add(closeButton)
|
||||||
|
|
||||||
|
aboutWindow.SetContent(content)
|
||||||
|
aboutWindow.Show()
|
||||||
|
}
|
||||||
|
|||||||
179
ui/optimization.go
Normal file
179
ui/optimization.go
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"fyne.io/fyne/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Debouncer struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
timer *time.Timer
|
||||||
|
duration time.Duration
|
||||||
|
callback func()
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDebouncer(duration time.Duration, callback func()) *Debouncer {
|
||||||
|
return &Debouncer{
|
||||||
|
duration: duration,
|
||||||
|
callback: callback,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Debouncer) Trigger() {
|
||||||
|
d.mu.Lock()
|
||||||
|
defer d.mu.Unlock()
|
||||||
|
|
||||||
|
if d.timer != nil {
|
||||||
|
d.timer.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
d.timer = time.AfterFunc(d.duration, func() {
|
||||||
|
fyne.Do(d.callback)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type Throttler struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
lastCall time.Time
|
||||||
|
interval time.Duration
|
||||||
|
callback func()
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewThrottler(interval time.Duration, callback func()) *Throttler {
|
||||||
|
return &Throttler{
|
||||||
|
interval: interval,
|
||||||
|
callback: callback,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Throttler) Trigger() {
|
||||||
|
t.mu.Lock()
|
||||||
|
defer t.mu.Unlock()
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
if now.Sub(t.lastCall) >= t.interval {
|
||||||
|
t.lastCall = now
|
||||||
|
fyne.Do(t.callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type LazyLoader struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
loaded bool
|
||||||
|
loading bool
|
||||||
|
callback func()
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLazyLoader(callback func()) *LazyLoader {
|
||||||
|
return &LazyLoader{
|
||||||
|
callback: callback,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LazyLoader) Load() {
|
||||||
|
l.mu.Lock()
|
||||||
|
defer l.mu.Unlock()
|
||||||
|
|
||||||
|
if l.loaded || l.loading {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l.loading = true
|
||||||
|
go func() {
|
||||||
|
l.callback()
|
||||||
|
l.mu.Lock()
|
||||||
|
l.loaded = true
|
||||||
|
l.loading = false
|
||||||
|
l.mu.Unlock()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
type VirtualList struct {
|
||||||
|
items []interface{}
|
||||||
|
renderer func(item interface{}) fyne.CanvasObject
|
||||||
|
visible int
|
||||||
|
start int
|
||||||
|
end int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewVirtualList(items []interface{}, renderer func(item interface{}) fyne.CanvasObject, visible int) *VirtualList {
|
||||||
|
return &VirtualList{
|
||||||
|
items: items,
|
||||||
|
renderer: renderer,
|
||||||
|
visible: visible,
|
||||||
|
end: visible,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (vl *VirtualList) GetVisibleItems() []fyne.CanvasObject {
|
||||||
|
if vl.start >= len(vl.items) {
|
||||||
|
return []fyne.CanvasObject{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if vl.end > len(vl.items) {
|
||||||
|
vl.end = len(vl.items)
|
||||||
|
}
|
||||||
|
|
||||||
|
var result []fyne.CanvasObject
|
||||||
|
for i := vl.start; i < vl.end; i++ {
|
||||||
|
result = append(result, vl.renderer(vl.items[i]))
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (vl *VirtualList) ScrollTo(index int) {
|
||||||
|
if index < 0 {
|
||||||
|
index = 0
|
||||||
|
}
|
||||||
|
if index >= len(vl.items) {
|
||||||
|
index = len(vl.items) - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
vl.start = index
|
||||||
|
vl.end = index + vl.visible
|
||||||
|
if vl.end > len(vl.items) {
|
||||||
|
vl.end = len(vl.items)
|
||||||
|
vl.start = vl.end - vl.visible
|
||||||
|
if vl.start < 0 {
|
||||||
|
vl.start = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type MemoryPool struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
items []interface{}
|
||||||
|
new func() interface{}
|
||||||
|
reset func(interface{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMemoryPool(new func() interface{}, reset func(interface{})) *MemoryPool {
|
||||||
|
return &MemoryPool{
|
||||||
|
new: new,
|
||||||
|
reset: reset,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mp *MemoryPool) Get() interface{} {
|
||||||
|
mp.mu.Lock()
|
||||||
|
defer mp.mu.Unlock()
|
||||||
|
|
||||||
|
if len(mp.items) > 0 {
|
||||||
|
item := mp.items[len(mp.items)-1]
|
||||||
|
mp.items = mp.items[:len(mp.items)-1]
|
||||||
|
mp.reset(item)
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
return mp.new()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mp *MemoryPool) Put(item interface{}) {
|
||||||
|
mp.mu.Lock()
|
||||||
|
defer mp.mu.Unlock()
|
||||||
|
|
||||||
|
mp.items = append(mp.items, item)
|
||||||
|
}
|
||||||
186
ui/settings_screen.go
Normal file
186
ui/settings_screen.go
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fyne.io/fyne/v2"
|
||||||
|
"fyne.io/fyne/v2/container"
|
||||||
|
"fyne.io/fyne/v2/widget"
|
||||||
|
|
||||||
|
"MobileMkch/cache"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SettingsScreen struct {
|
||||||
|
uiManager *UIManager
|
||||||
|
content *fyne.Container
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSettingsScreen(uiManager *UIManager) *SettingsScreen {
|
||||||
|
screen := &SettingsScreen{
|
||||||
|
uiManager: uiManager,
|
||||||
|
content: container.NewVBox(),
|
||||||
|
}
|
||||||
|
screen.setupContent()
|
||||||
|
return screen
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ss *SettingsScreen) setupContent() {
|
||||||
|
ss.content.RemoveAll()
|
||||||
|
|
||||||
|
header := widget.NewLabel("Настройки")
|
||||||
|
header.TextStyle = fyne.TextStyle{Bold: true}
|
||||||
|
ss.content.Add(header)
|
||||||
|
|
||||||
|
themeLabel := widget.NewLabel("Тема:")
|
||||||
|
themeSelect := widget.NewSelect([]string{"Темная", "Светлая"}, func(theme string) {
|
||||||
|
if theme == "Темная" {
|
||||||
|
ss.uiManager.GetSettings().Theme = "dark"
|
||||||
|
} else {
|
||||||
|
ss.uiManager.GetSettings().Theme = "light"
|
||||||
|
}
|
||||||
|
ss.uiManager.SaveSettings()
|
||||||
|
})
|
||||||
|
|
||||||
|
if ss.uiManager.GetSettings().Theme == "dark" {
|
||||||
|
themeSelect.SetSelected("Темная")
|
||||||
|
} else {
|
||||||
|
themeSelect.SetSelected("Светлая")
|
||||||
|
}
|
||||||
|
|
||||||
|
themeContainer := container.NewHBox(themeLabel, themeSelect)
|
||||||
|
ss.content.Add(themeContainer)
|
||||||
|
|
||||||
|
autoRefreshCheck := widget.NewCheck("Авторефреш", func(checked bool) {
|
||||||
|
ss.uiManager.GetSettings().AutoRefresh = checked
|
||||||
|
ss.uiManager.SaveSettings()
|
||||||
|
})
|
||||||
|
autoRefreshCheck.SetChecked(ss.uiManager.GetSettings().AutoRefresh)
|
||||||
|
ss.content.Add(autoRefreshCheck)
|
||||||
|
|
||||||
|
showFilesCheck := widget.NewCheck("Показывать файлы", func(checked bool) {
|
||||||
|
ss.uiManager.GetSettings().ShowFiles = checked
|
||||||
|
ss.uiManager.SaveSettings()
|
||||||
|
})
|
||||||
|
showFilesCheck.SetChecked(ss.uiManager.GetSettings().ShowFiles)
|
||||||
|
ss.content.Add(showFilesCheck)
|
||||||
|
|
||||||
|
compactModeCheck := widget.NewCheck("Компактный режим", func(checked bool) {
|
||||||
|
ss.uiManager.GetSettings().CompactMode = checked
|
||||||
|
ss.uiManager.SaveSettings()
|
||||||
|
})
|
||||||
|
compactModeCheck.SetChecked(ss.uiManager.GetSettings().CompactMode)
|
||||||
|
ss.content.Add(compactModeCheck)
|
||||||
|
|
||||||
|
pageSizeLabel := widget.NewLabel("Размер страницы:")
|
||||||
|
pageSizeSelect := widget.NewSelect([]string{"5", "10", "15", "20"}, func(size string) {
|
||||||
|
switch size {
|
||||||
|
case "5":
|
||||||
|
ss.uiManager.GetSettings().PageSize = 5
|
||||||
|
case "10":
|
||||||
|
ss.uiManager.GetSettings().PageSize = 10
|
||||||
|
case "15":
|
||||||
|
ss.uiManager.GetSettings().PageSize = 15
|
||||||
|
case "20":
|
||||||
|
ss.uiManager.GetSettings().PageSize = 20
|
||||||
|
}
|
||||||
|
ss.uiManager.SaveSettings()
|
||||||
|
})
|
||||||
|
|
||||||
|
currentSize := ss.uiManager.GetSettings().PageSize
|
||||||
|
switch currentSize {
|
||||||
|
case 5:
|
||||||
|
pageSizeSelect.SetSelected("5")
|
||||||
|
case 10:
|
||||||
|
pageSizeSelect.SetSelected("10")
|
||||||
|
case 15:
|
||||||
|
pageSizeSelect.SetSelected("15")
|
||||||
|
case 20:
|
||||||
|
pageSizeSelect.SetSelected("20")
|
||||||
|
default:
|
||||||
|
pageSizeSelect.SetSelected("10")
|
||||||
|
}
|
||||||
|
|
||||||
|
pageSizeContainer := container.NewHBox(pageSizeLabel, pageSizeSelect)
|
||||||
|
ss.content.Add(pageSizeContainer)
|
||||||
|
|
||||||
|
lastBoardLabel := widget.NewLabel("Последняя доска:")
|
||||||
|
lastBoardValue := widget.NewLabel(ss.uiManager.GetLastBoard())
|
||||||
|
if lastBoardValue.Text == "" {
|
||||||
|
lastBoardValue.SetText("Не выбрана")
|
||||||
|
}
|
||||||
|
lastBoardContainer := container.NewHBox(lastBoardLabel, lastBoardValue)
|
||||||
|
ss.content.Add(lastBoardContainer)
|
||||||
|
|
||||||
|
ss.content.Add(widget.NewSeparator())
|
||||||
|
cacheHeader := widget.NewLabel("Управление кэшем")
|
||||||
|
cacheHeader.TextStyle = fyne.TextStyle{Bold: true}
|
||||||
|
ss.content.Add(cacheHeader)
|
||||||
|
clearBoardsCacheBtn := widget.NewButton("Очистить кэш досок", func() {
|
||||||
|
cache.GetCache().Delete("boards")
|
||||||
|
ss.uiManager.ShowInfo("Кэш очищен", "Кэш досок очищен")
|
||||||
|
})
|
||||||
|
ss.content.Add(clearBoardsCacheBtn)
|
||||||
|
clearThreadsCacheBtn := widget.NewButton("Очистить кэш тредов", func() {
|
||||||
|
boards, err := ss.uiManager.GetAPIClient().GetBoards()
|
||||||
|
if err == nil {
|
||||||
|
for _, board := range boards {
|
||||||
|
cache.GetCache().Delete("threads_" + board.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ss.uiManager.ShowInfo("Кэш очищен", "Кэш тредов для всех досок очищен")
|
||||||
|
})
|
||||||
|
ss.content.Add(clearThreadsCacheBtn)
|
||||||
|
clearThreadDetailsCacheBtn := widget.NewButton("Очистить кэш деталей тредов", func() {
|
||||||
|
// Удаляем кэш деталей тредов и комментариев
|
||||||
|
// Это более сложно, так как нужно знать ID тредов
|
||||||
|
// Пока просто очищаем весь кэш
|
||||||
|
cache.GetCache().Clear()
|
||||||
|
ss.uiManager.ShowInfo("Кэш очищен", "Кэш деталей тредов очищен")
|
||||||
|
})
|
||||||
|
ss.content.Add(clearThreadDetailsCacheBtn)
|
||||||
|
|
||||||
|
clearAllCacheBtn := widget.NewButton("Очистить весь кэш", func() {
|
||||||
|
cache.GetCache().Clear()
|
||||||
|
ss.uiManager.ShowInfo("Кэш очищен", "Весь кэш очищен")
|
||||||
|
})
|
||||||
|
ss.content.Add(clearAllCacheBtn)
|
||||||
|
ss.content.Add(widget.NewSeparator())
|
||||||
|
resetHeader := widget.NewLabel("Сброс настроек")
|
||||||
|
resetHeader.TextStyle = fyne.TextStyle{Bold: true}
|
||||||
|
ss.content.Add(resetHeader)
|
||||||
|
|
||||||
|
resetButton := widget.NewButton("Сбросить настройки", func() {
|
||||||
|
ss.uiManager.GetSettings().Theme = "dark"
|
||||||
|
ss.uiManager.GetSettings().AutoRefresh = true
|
||||||
|
ss.uiManager.GetSettings().ShowFiles = true
|
||||||
|
ss.uiManager.GetSettings().CompactMode = false
|
||||||
|
ss.uiManager.GetSettings().LastBoard = ""
|
||||||
|
ss.uiManager.GetSettings().PageSize = 10
|
||||||
|
ss.uiManager.SaveSettings()
|
||||||
|
ss.setupContent() // Перезагружаем интерфейс
|
||||||
|
})
|
||||||
|
ss.content.Add(resetButton)
|
||||||
|
|
||||||
|
ss.content.Add(widget.NewSeparator())
|
||||||
|
|
||||||
|
aboutButton := widget.NewButton("Об аппке", func() {
|
||||||
|
ss.uiManager.ShowAbout()
|
||||||
|
})
|
||||||
|
ss.content.Add(aboutButton)
|
||||||
|
|
||||||
|
ss.content.Refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ss *SettingsScreen) GetContent() fyne.CanvasObject {
|
||||||
|
return ss.content
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ss *SettingsScreen) GetTitle() string {
|
||||||
|
return "Настройки"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ss *SettingsScreen) OnShow() {
|
||||||
|
ss.setupContent()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ss *SettingsScreen) OnHide() {
|
||||||
|
ss.uiManager.SaveSettings()
|
||||||
|
}
|
||||||
@ -170,15 +170,14 @@ func (tds *ThreadDetailScreen) displayThreadDetail() {
|
|||||||
func (tds *ThreadDetailScreen) createThreadContainer(thread *models.ThreadDetail) *fyne.Container {
|
func (tds *ThreadDetailScreen) createThreadContainer(thread *models.ThreadDetail) *fyne.Container {
|
||||||
container := container.NewVBox()
|
container := container.NewVBox()
|
||||||
|
|
||||||
info := fmt.Sprintf("Автор: %s | Дата: %s",
|
info := fmt.Sprintf("Дата: %s",
|
||||||
thread.Author,
|
|
||||||
thread.GetCreationTime().Format("02.01.2006 15:04"))
|
thread.GetCreationTime().Format("02.01.2006 15:04"))
|
||||||
|
|
||||||
infoLabel := widget.NewLabel(info)
|
infoLabel := widget.NewLabel(info)
|
||||||
infoLabel.TextStyle = fyne.TextStyle{Italic: true}
|
infoLabel.TextStyle = fyne.TextStyle{Italic: true}
|
||||||
container.Add(infoLabel)
|
container.Add(infoLabel)
|
||||||
|
|
||||||
if len(thread.Files) > 0 {
|
if len(thread.Files) > 0 && tds.uiManager.GetSettings().ShowFiles {
|
||||||
filesContainer := tds.createFilesContainer(thread.Files)
|
filesContainer := tds.createFilesContainer(thread.Files)
|
||||||
container.Add(filesContainer)
|
container.Add(filesContainer)
|
||||||
}
|
}
|
||||||
@ -203,7 +202,7 @@ func (tds *ThreadDetailScreen) createCommentContainer(comment *models.Comment) *
|
|||||||
infoLabel.TextStyle = fyne.TextStyle{Italic: true}
|
infoLabel.TextStyle = fyne.TextStyle{Italic: true}
|
||||||
container.Add(infoLabel)
|
container.Add(infoLabel)
|
||||||
|
|
||||||
if len(comment.Files) > 0 {
|
if len(comment.Files) > 0 && tds.uiManager.GetSettings().ShowFiles {
|
||||||
filesContainer := tds.createFilesContainer(comment.Files)
|
filesContainer := tds.createFilesContainer(comment.Files)
|
||||||
container.Add(filesContainer)
|
container.Add(filesContainer)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"fyne.io/fyne/v2"
|
"fyne.io/fyne/v2"
|
||||||
"fyne.io/fyne/v2/container"
|
"fyne.io/fyne/v2/container"
|
||||||
@ -13,11 +14,16 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ThreadsScreen struct {
|
type ThreadsScreen struct {
|
||||||
uiManager *UIManager
|
uiManager *UIManager
|
||||||
board *models.Board
|
board *models.Board
|
||||||
content *fyne.Container
|
content *fyne.Container
|
||||||
threads []models.Thread
|
threads []models.Thread
|
||||||
loading bool
|
loading bool
|
||||||
|
debouncer *Debouncer
|
||||||
|
currentPage int
|
||||||
|
pageSize int
|
||||||
|
totalPages int
|
||||||
|
pageContainer *fyne.Container
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewThreadsScreen(uiManager *UIManager, board *models.Board) *ThreadsScreen {
|
func NewThreadsScreen(uiManager *UIManager, board *models.Board) *ThreadsScreen {
|
||||||
@ -25,8 +31,13 @@ func NewThreadsScreen(uiManager *UIManager, board *models.Board) *ThreadsScreen
|
|||||||
uiManager: uiManager,
|
uiManager: uiManager,
|
||||||
board: board,
|
board: board,
|
||||||
loading: false,
|
loading: false,
|
||||||
|
pageSize: uiManager.GetSettings().PageSize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
screen.debouncer = NewDebouncer(100*time.Millisecond, func() {
|
||||||
|
screen.displayThreads()
|
||||||
|
})
|
||||||
|
|
||||||
screen.setupContent()
|
screen.setupContent()
|
||||||
return screen
|
return screen
|
||||||
}
|
}
|
||||||
@ -82,7 +93,7 @@ func (ts *ThreadsScreen) loadThreads() {
|
|||||||
ts.threads = threads
|
ts.threads = threads
|
||||||
|
|
||||||
fyne.Do(func() {
|
fyne.Do(func() {
|
||||||
ts.displayThreads()
|
ts.debouncer.Trigger()
|
||||||
})
|
})
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
@ -135,25 +146,35 @@ func (ts *ThreadsScreen) displayThreads() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := range ts.threads {
|
ts.totalPages = (len(ts.threads) + ts.pageSize - 1) / ts.pageSize
|
||||||
thread := ts.threads[i]
|
ts.pageContainer = container.NewVBox()
|
||||||
|
|
||||||
threadCard := ts.createThreadCard(&thread)
|
start := ts.currentPage * ts.pageSize
|
||||||
ts.content.Add(threadCard)
|
end := start + ts.pageSize
|
||||||
|
if end > len(ts.threads) {
|
||||||
|
end = len(ts.threads)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for i := start; i < end; i++ {
|
||||||
|
thread := &ts.threads[i]
|
||||||
|
threadCard := ts.createThreadCard(thread)
|
||||||
|
ts.pageContainer.Add(threadCard)
|
||||||
|
}
|
||||||
|
|
||||||
|
ts.addPaginationControls()
|
||||||
|
|
||||||
|
ts.content.Add(ts.pageContainer)
|
||||||
ts.content.Refresh()
|
ts.content.Refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ts *ThreadsScreen) createThreadCard(thread *models.Thread) fyne.CanvasObject {
|
func (ts *ThreadsScreen) createThreadCard(thread *models.Thread) fyne.CanvasObject {
|
||||||
title := fmt.Sprintf("#%d: %s", thread.ID, thread.Title)
|
title := fmt.Sprintf("#%d: %s", thread.ID, thread.Title)
|
||||||
|
|
||||||
if len(title) > 50 {
|
if len(title) > 20 {
|
||||||
title = title[:47] + "..."
|
title = title[:17] + "..."
|
||||||
}
|
}
|
||||||
|
|
||||||
info := fmt.Sprintf("Автор: %s | %s",
|
info := fmt.Sprintf("Дата: %s",
|
||||||
thread.Author,
|
|
||||||
thread.GetCreationTime().Format("02.01.2006 15:04"))
|
thread.GetCreationTime().Format("02.01.2006 15:04"))
|
||||||
|
|
||||||
if thread.IsPinned() {
|
if thread.IsPinned() {
|
||||||
@ -164,7 +185,7 @@ func (ts *ThreadsScreen) createThreadCard(thread *models.Thread) fyne.CanvasObje
|
|||||||
info += fmt.Sprintf(" | ⭐ %d", rating)
|
info += fmt.Sprintf(" | ⭐ %d", rating)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(thread.Files) > 0 {
|
if len(thread.Files) > 0 && ts.uiManager.GetSettings().ShowFiles {
|
||||||
info += fmt.Sprintf(" | 📎 %d", len(thread.Files))
|
info += fmt.Sprintf(" | 📎 %d", len(thread.Files))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -193,3 +214,45 @@ func (ts *ThreadsScreen) createThreadCard(thread *models.Thread) fyne.CanvasObje
|
|||||||
func (ts *ThreadsScreen) RefreshThreads() {
|
func (ts *ThreadsScreen) RefreshThreads() {
|
||||||
ts.loadThreads()
|
ts.loadThreads()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ts *ThreadsScreen) addPaginationControls() {
|
||||||
|
if ts.totalPages <= 1 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
paginationContainer := container.NewHBox()
|
||||||
|
|
||||||
|
prevButton := widget.NewButton("←", func() {
|
||||||
|
if ts.currentPage > 0 {
|
||||||
|
ts.currentPage--
|
||||||
|
ts.displayThreads()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if ts.currentPage == 0 {
|
||||||
|
prevButton.Disable()
|
||||||
|
}
|
||||||
|
paginationContainer.Add(prevButton)
|
||||||
|
|
||||||
|
pageInfo := widget.NewLabel(fmt.Sprintf("Страница %d из %d", ts.currentPage+1, ts.totalPages))
|
||||||
|
paginationContainer.Add(pageInfo)
|
||||||
|
|
||||||
|
nextButton := widget.NewButton("→", func() {
|
||||||
|
if ts.currentPage < ts.totalPages-1 {
|
||||||
|
ts.currentPage++
|
||||||
|
ts.displayThreads()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if ts.currentPage >= ts.totalPages-1 {
|
||||||
|
nextButton.Disable()
|
||||||
|
}
|
||||||
|
paginationContainer.Add(nextButton)
|
||||||
|
|
||||||
|
ts.pageContainer.Add(paginationContainer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ts *ThreadsScreen) goToPage(page int) {
|
||||||
|
if page >= 0 && page < ts.totalPages {
|
||||||
|
ts.currentPage = page
|
||||||
|
ts.displayThreads()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user