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
|
||||
|
||||
**✅ iOS сборка протестирована и работает!**
|
||||
**✅ iOS И Android сборка протестирована и работает!**
|
||||
|
||||
|
||||
## Требования
|
||||
|
||||
@ -92,8 +116,13 @@ open MobileMkch.xcodeproj
|
||||
- `main.go` - точка входа
|
||||
- `api/client.go` - HTTP клиент для mkch API
|
||||
- `models/models.go` - структуры данных
|
||||
- `settings/settings.go` - система настроек
|
||||
- `cache/cache.go` - система кэширования с поддержкой пагинации
|
||||
- `ui/` - пользовательский интерфейс
|
||||
- `manager.go` - управление экранами
|
||||
- `boards_screen.go` - список досок
|
||||
- `threads_screen.go` - треды доски
|
||||
- `thread_detail_screen.go` - детали треда
|
||||
- `settings_screen.go` - экран настроек
|
||||
- `optimization.go` - утилиты оптимизации
|
||||
- `mobile_optimizations.go` - оптимизации для мобильных устройств
|
||||
@ -7,6 +7,7 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"MobileMkch/cache"
|
||||
"MobileMkch/models"
|
||||
)
|
||||
|
||||
@ -69,6 +70,14 @@ func (c *Client) makeRequest(url string) ([]byte, 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/"
|
||||
body, err := c.makeRequest(url)
|
||||
if err != nil {
|
||||
@ -84,6 +93,9 @@ func (c *Client) GetBoards() ([]models.Board, error) {
|
||||
return nil, fmt.Errorf("ошибка парсинга досок: %w", err)
|
||||
}
|
||||
|
||||
// Всех победили
|
||||
cache.GetCache().SetBoards(boards)
|
||||
|
||||
if c.debug {
|
||||
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) {
|
||||
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)
|
||||
body, err := c.makeRequest(url)
|
||||
if err != nil {
|
||||
@ -107,6 +126,15 @@ 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 {
|
||||
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) {
|
||||
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)
|
||||
body, err := c.makeRequest(url)
|
||||
if err != nil {
|
||||
@ -130,6 +165,8 @@ func (c *Client) GetThread(boardCode string, threadID int) (*models.ThreadDetail
|
||||
return nil, fmt.Errorf("ошибка парсинга треда: %w", err)
|
||||
}
|
||||
|
||||
cache.GetCache().SetThreadDetail(threadID, &thread)
|
||||
|
||||
if c.debug {
|
||||
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) {
|
||||
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)
|
||||
body, err := c.makeRequest(url)
|
||||
if err != nil {
|
||||
@ -153,6 +197,8 @@ func (c *Client) GetComments(boardCode string, threadID int) ([]models.Comment,
|
||||
return nil, fmt.Errorf("ошибка парсинга комментариев: %w", err)
|
||||
}
|
||||
|
||||
cache.GetCache().SetComments(threadID, comments)
|
||||
|
||||
if c.debug {
|
||||
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"
|
||||
|
||||
"MobileMkch/api"
|
||||
"MobileMkch/settings"
|
||||
"MobileMkch/ui"
|
||||
)
|
||||
|
||||
@ -17,7 +18,9 @@ func main() {
|
||||
a := app.NewWithID("com.mkch.mobile")
|
||||
a.SetIcon(theme.ComputerIcon())
|
||||
|
||||
a.Settings().SetTheme(theme.DarkTheme())
|
||||
settings.SetApp(a)
|
||||
|
||||
MobileOptimizations(a)
|
||||
|
||||
w := a.NewWindow("MobileMkch")
|
||||
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"`
|
||||
Text string `json:"text"`
|
||||
Creation string `json:"creation"`
|
||||
Author string `json:"author"`
|
||||
Board string `json:"board"`
|
||||
Rating *int `json:"rating,omitempty"`
|
||||
Pinned *bool `json:"pinned,omitempty"`
|
||||
@ -45,7 +44,6 @@ type ThreadDetail struct {
|
||||
Creation string `json:"creation"`
|
||||
Title string `json:"title"`
|
||||
Text string `json:"text"`
|
||||
Author string `json:"author"`
|
||||
Board string `json:"board"`
|
||||
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() {
|
||||
bs.uiManager.SetLastBoard(board.Code)
|
||||
bs.uiManager.ShowBoardThreads(board)
|
||||
})
|
||||
|
||||
|
||||
108
ui/manager.go
108
ui/manager.go
@ -1,13 +1,14 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"MobileMkch/api"
|
||||
"MobileMkch/models"
|
||||
"MobileMkch/settings"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/theme"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
|
||||
"MobileMkch/api"
|
||||
"MobileMkch/models"
|
||||
)
|
||||
|
||||
type UIManager struct {
|
||||
@ -22,7 +23,7 @@ type UIManager struct {
|
||||
headerBar *fyne.Container
|
||||
content *fyne.Container
|
||||
|
||||
isDarkTheme bool
|
||||
settings *settings.Settings
|
||||
}
|
||||
|
||||
type Screen interface {
|
||||
@ -33,13 +34,30 @@ type Screen interface {
|
||||
}
|
||||
|
||||
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{
|
||||
app: app,
|
||||
window: window,
|
||||
apiClient: apiClient,
|
||||
navigationStack: make([]Screen, 0),
|
||||
content: container.NewMax(),
|
||||
isDarkTheme: true, // По умолчанию темная тема
|
||||
settings: appSettings,
|
||||
}
|
||||
|
||||
if appSettings.Theme == "dark" {
|
||||
app.Settings().SetTheme(theme.DarkTheme())
|
||||
} else {
|
||||
app.Settings().SetTheme(theme.LightTheme())
|
||||
}
|
||||
|
||||
manager.setupHeader()
|
||||
@ -52,30 +70,30 @@ func NewUIManager(app fyne.App, window fyne.Window, apiClient *api.Client) *UIMa
|
||||
}
|
||||
|
||||
func (ui *UIManager) setupHeader() {
|
||||
titleLabel := widget.NewLabel("MobileMkch")
|
||||
titleLabel.TextStyle = fyne.TextStyle{Bold: true}
|
||||
|
||||
backButton := widget.NewButton("← Назад", func() {
|
||||
backButton := widget.NewButton("<- Назад", func() {
|
||||
ui.GoBack()
|
||||
})
|
||||
backButton.Hide()
|
||||
|
||||
titleLabel := widget.NewLabel("MobileMkch")
|
||||
titleLabel.TextStyle = fyne.TextStyle{Bold: true}
|
||||
titleLabel.Alignment = fyne.TextAlignCenter
|
||||
|
||||
refreshButton := widget.NewButton("🔄", func() {
|
||||
if ui.currentScreen != nil {
|
||||
ui.currentScreen.OnShow()
|
||||
}
|
||||
})
|
||||
|
||||
themeButton := widget.NewButton("🌙", func() {
|
||||
ui.toggleTheme()
|
||||
settingsButton := widget.NewButton("⚙️", func() {
|
||||
settingsScreen := NewSettingsScreen(ui)
|
||||
ui.ShowScreen(settingsScreen)
|
||||
})
|
||||
|
||||
rightButtons := container.NewHBox(refreshButton, themeButton)
|
||||
rightButtons := container.NewHBox(refreshButton, settingsButton)
|
||||
|
||||
ui.headerBar = container.NewHBox(
|
||||
backButton,
|
||||
titleLabel,
|
||||
rightButtons,
|
||||
ui.headerBar = container.NewBorder(
|
||||
nil, nil, backButton, rightButtons, titleLabel,
|
||||
)
|
||||
}
|
||||
|
||||
@ -100,12 +118,10 @@ func (ui *UIManager) ShowScreen(screen Screen) {
|
||||
|
||||
ui.navigationStack = append(ui.navigationStack, screen)
|
||||
ui.currentScreen = screen
|
||||
|
||||
ui.content.RemoveAll()
|
||||
ui.content.Add(screen.GetContent())
|
||||
ui.content.Refresh()
|
||||
|
||||
ui.updateHeader(screen.GetTitle())
|
||||
ui.content.Refresh()
|
||||
|
||||
screen.OnShow()
|
||||
}
|
||||
@ -132,22 +148,23 @@ func (ui *UIManager) GoBack() {
|
||||
}
|
||||
|
||||
func (ui *UIManager) toggleTheme() {
|
||||
ui.isDarkTheme = !ui.isDarkTheme
|
||||
|
||||
if ui.isDarkTheme {
|
||||
ui.app.Settings().SetTheme(theme.DarkTheme())
|
||||
} else {
|
||||
if ui.settings.Theme == "dark" {
|
||||
ui.settings.Theme = "light"
|
||||
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())
|
||||
}
|
||||
|
||||
func (ui *UIManager) updateHeader(title string) {
|
||||
ui.window.SetTitle(title)
|
||||
|
||||
if len(ui.headerBar.Objects) > 0 {
|
||||
if backBtn, ok := ui.headerBar.Objects[0].(*widget.Button); ok {
|
||||
if len(ui.headerBar.Objects) >= 3 {
|
||||
if backBtn, ok := ui.headerBar.Objects[1].(*widget.Button); ok {
|
||||
if len(ui.navigationStack) > 1 {
|
||||
backBtn.Show()
|
||||
} else {
|
||||
@ -208,3 +225,42 @@ func (ui *UIManager) ShowInfo(title, message string) {
|
||||
)
|
||||
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 {
|
||||
container := container.NewVBox()
|
||||
|
||||
info := fmt.Sprintf("Автор: %s | Дата: %s",
|
||||
thread.Author,
|
||||
info := fmt.Sprintf("Дата: %s",
|
||||
thread.GetCreationTime().Format("02.01.2006 15:04"))
|
||||
|
||||
infoLabel := widget.NewLabel(info)
|
||||
infoLabel.TextStyle = fyne.TextStyle{Italic: true}
|
||||
container.Add(infoLabel)
|
||||
|
||||
if len(thread.Files) > 0 {
|
||||
if len(thread.Files) > 0 && tds.uiManager.GetSettings().ShowFiles {
|
||||
filesContainer := tds.createFilesContainer(thread.Files)
|
||||
container.Add(filesContainer)
|
||||
}
|
||||
@ -203,7 +202,7 @@ func (tds *ThreadDetailScreen) createCommentContainer(comment *models.Comment) *
|
||||
infoLabel.TextStyle = fyne.TextStyle{Italic: true}
|
||||
container.Add(infoLabel)
|
||||
|
||||
if len(comment.Files) > 0 {
|
||||
if len(comment.Files) > 0 && tds.uiManager.GetSettings().ShowFiles {
|
||||
filesContainer := tds.createFilesContainer(comment.Files)
|
||||
container.Add(filesContainer)
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/container"
|
||||
@ -18,6 +19,11 @@ type ThreadsScreen struct {
|
||||
content *fyne.Container
|
||||
threads []models.Thread
|
||||
loading bool
|
||||
debouncer *Debouncer
|
||||
currentPage int
|
||||
pageSize int
|
||||
totalPages int
|
||||
pageContainer *fyne.Container
|
||||
}
|
||||
|
||||
func NewThreadsScreen(uiManager *UIManager, board *models.Board) *ThreadsScreen {
|
||||
@ -25,8 +31,13 @@ func NewThreadsScreen(uiManager *UIManager, board *models.Board) *ThreadsScreen
|
||||
uiManager: uiManager,
|
||||
board: board,
|
||||
loading: false,
|
||||
pageSize: uiManager.GetSettings().PageSize,
|
||||
}
|
||||
|
||||
screen.debouncer = NewDebouncer(100*time.Millisecond, func() {
|
||||
screen.displayThreads()
|
||||
})
|
||||
|
||||
screen.setupContent()
|
||||
return screen
|
||||
}
|
||||
@ -82,7 +93,7 @@ func (ts *ThreadsScreen) loadThreads() {
|
||||
ts.threads = threads
|
||||
|
||||
fyne.Do(func() {
|
||||
ts.displayThreads()
|
||||
ts.debouncer.Trigger()
|
||||
})
|
||||
}()
|
||||
}
|
||||
@ -135,25 +146,35 @@ func (ts *ThreadsScreen) displayThreads() {
|
||||
return
|
||||
}
|
||||
|
||||
for i := range ts.threads {
|
||||
thread := ts.threads[i]
|
||||
ts.totalPages = (len(ts.threads) + ts.pageSize - 1) / ts.pageSize
|
||||
ts.pageContainer = container.NewVBox()
|
||||
|
||||
threadCard := ts.createThreadCard(&thread)
|
||||
ts.content.Add(threadCard)
|
||||
start := ts.currentPage * ts.pageSize
|
||||
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()
|
||||
}
|
||||
|
||||
func (ts *ThreadsScreen) createThreadCard(thread *models.Thread) fyne.CanvasObject {
|
||||
title := fmt.Sprintf("#%d: %s", thread.ID, thread.Title)
|
||||
|
||||
if len(title) > 50 {
|
||||
title = title[:47] + "..."
|
||||
if len(title) > 20 {
|
||||
title = title[:17] + "..."
|
||||
}
|
||||
|
||||
info := fmt.Sprintf("Автор: %s | %s",
|
||||
thread.Author,
|
||||
info := fmt.Sprintf("Дата: %s",
|
||||
thread.GetCreationTime().Format("02.01.2006 15:04"))
|
||||
|
||||
if thread.IsPinned() {
|
||||
@ -164,7 +185,7 @@ func (ts *ThreadsScreen) createThreadCard(thread *models.Thread) fyne.CanvasObje
|
||||
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))
|
||||
}
|
||||
|
||||
@ -193,3 +214,45 @@ func (ts *ThreadsScreen) createThreadCard(thread *models.Thread) fyne.CanvasObje
|
||||
func (ts *ThreadsScreen) RefreshThreads() {
|
||||
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