Добавить тесты репозитория файловой системы и реализовать функциональность журналирования файлов.
- Реализовать тесты для поиска MP3-файлов и переименования/организации папок книг в репозитории файловой системы. - Создать FileLogger для записи сообщений в файл с поддержкой различных уровней журналирования и управления размером файлов. - Разработать репозиторий RuTracker для обработки поиска торрентов, получения метаданных и загрузки торрент-файлов. - Добавить тесты для нормализации URL в репозиторий RuTracker. - Реализовать адаптер логгера TUI для отображения логов в терминальном интерфейсе и, при необходимости, для записи логов в базовый логгер. - Создать менеджер TUI для управления пользовательским интерфейсом приложения, включая главное меню, экран обработки, настройки и отображение результатов.
This commit is contained in:
343
internal/presentation/tui/manager.go
Normal file
343
internal/presentation/tui/manager.go
Normal file
@@ -0,0 +1,343 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"audio-catalyst/internal/domain/entities"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/rivo/tview"
|
||||
)
|
||||
|
||||
// Manager управляет TUI интерфейсом
|
||||
type Manager struct {
|
||||
app *tview.Application
|
||||
pages *tview.Pages
|
||||
currentScreen entities.UIScreen
|
||||
|
||||
// Основные компоненты
|
||||
mainFlex *tview.Flex
|
||||
headerBar *tview.TextView
|
||||
footerBar *tview.TextView
|
||||
statusBar *tview.TextView
|
||||
|
||||
// Компоненты главного меню
|
||||
menuList *tview.List
|
||||
infoPanel *tview.TextView
|
||||
|
||||
// Компоненты обработки
|
||||
logView *tview.TextView
|
||||
progressBar *tview.TextView
|
||||
progressText *tview.TextView
|
||||
|
||||
// Компоненты настроек
|
||||
settingsForm *tview.Form
|
||||
|
||||
// Компоненты результатов
|
||||
resultsTable *tview.Table
|
||||
detailView *tview.TextView
|
||||
|
||||
// Состояние
|
||||
logBuffer []string
|
||||
maxLogLines int
|
||||
mu sync.Mutex
|
||||
|
||||
// Каналы для обновлений
|
||||
statusUpdate chan entities.ProcessingStatus
|
||||
logUpdate chan string
|
||||
|
||||
// Колбэки действий UI
|
||||
onStartProcessing func()
|
||||
}
|
||||
|
||||
// NewManager создает новый TUI менеджер
|
||||
func NewManager() *Manager {
|
||||
return &Manager{
|
||||
app: tview.NewApplication(),
|
||||
maxLogLines: 100,
|
||||
logBuffer: make([]string, 0),
|
||||
statusUpdate: make(chan entities.ProcessingStatus, 100),
|
||||
logUpdate: make(chan string, 100),
|
||||
}
|
||||
}
|
||||
|
||||
// SetOnStartProcessing регистрирует обработчик запуска обработки
|
||||
func (m *Manager) SetOnStartProcessing(handler func()) {
|
||||
m.onStartProcessing = handler
|
||||
}
|
||||
|
||||
// Initialize инициализирует UI компоненты
|
||||
func (m *Manager) Initialize() {
|
||||
m.initializeComponents()
|
||||
m.setupMainMenu()
|
||||
m.setupProcessingScreen()
|
||||
m.setupSettingsScreen()
|
||||
m.setupResultsScreen()
|
||||
m.setupPages()
|
||||
}
|
||||
|
||||
// Run запускает TUI приложение
|
||||
func (m *Manager) Run() error {
|
||||
go m.handleUpdates()
|
||||
return m.app.Run()
|
||||
}
|
||||
|
||||
// Stop останавливает TUI приложение
|
||||
func (m *Manager) Stop() {
|
||||
m.app.Stop()
|
||||
}
|
||||
|
||||
// SendStatusUpdate отправляет обновление статуса
|
||||
func (m *Manager) SendStatusUpdate(status entities.ProcessingStatus) {
|
||||
select {
|
||||
case m.statusUpdate <- status:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
// SendLogUpdate отправляет обновление лога
|
||||
func (m *Manager) SendLogUpdate(message string) {
|
||||
select {
|
||||
case m.logUpdate <- message:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
// initializeComponents инициализирует основные компоненты
|
||||
func (m *Manager) initializeComponents() {
|
||||
// Создаем основные элементы
|
||||
m.headerBar = tview.NewTextView().
|
||||
SetDynamicColors(true).
|
||||
SetTextAlign(tview.AlignCenter).
|
||||
SetText("[blue::b]🎵 AudioBook Catalyst v1.0 🎵[-:-:-]")
|
||||
m.headerBar.SetBorder(true)
|
||||
|
||||
m.footerBar = tview.NewTextView().
|
||||
SetDynamicColors(true).
|
||||
SetTextAlign(tview.AlignCenter).
|
||||
SetText("[grey]ESC: Выход | TAB: Навигация | ENTER: Выбор[-]")
|
||||
|
||||
m.statusBar = tview.NewTextView().
|
||||
SetDynamicColors(true).
|
||||
SetText("[green]Готов к работе[-]")
|
||||
|
||||
// Создаем систему страниц заранее
|
||||
m.pages = tview.NewPages()
|
||||
|
||||
// Создаем главный контейнер: Header | Pages | Status | Footer
|
||||
m.mainFlex = tview.NewFlex().
|
||||
SetDirection(tview.FlexRow).
|
||||
AddItem(m.headerBar, 3, 0, false).
|
||||
AddItem(m.pages, 0, 1, true).
|
||||
AddItem(m.statusBar, 1, 0, false).
|
||||
AddItem(m.footerBar, 1, 0, false)
|
||||
}
|
||||
|
||||
// setupMainMenu настраивает главное меню
|
||||
func (m *Manager) setupMainMenu() {
|
||||
m.menuList = tview.NewList().
|
||||
AddItem("🚀 Начать обработку аудиокниг", "Начать сканирование и обработку аудиокниг", '1', func() {
|
||||
m.switchToProcessing()
|
||||
if m.onStartProcessing != nil {
|
||||
go m.onStartProcessing()
|
||||
}
|
||||
}).
|
||||
AddItem("⚙️ Настройки", "Настройка параметров приложения", '2', func() {
|
||||
m.switchToSettings()
|
||||
}).
|
||||
AddItem("📊 Результаты", "Просмотр результатов обработки", '3', func() {
|
||||
m.switchToResults()
|
||||
}).
|
||||
AddItem("❌ Выход", "Завершить работу приложения", 'q', func() {
|
||||
m.app.Stop()
|
||||
})
|
||||
|
||||
m.menuList.SetBorder(true).SetTitle(" Главное меню ")
|
||||
|
||||
m.infoPanel = tview.NewTextView().
|
||||
SetDynamicColors(true).
|
||||
SetWrap(true).
|
||||
SetText(`[yellow]AudioBook Catalyst[-]
|
||||
|
||||
Приложение для автоматической обработки аудиокниг:
|
||||
• Сканирование папок с MP3 файлами
|
||||
• Поиск метаданных на RuTracker
|
||||
• Создание файлов metadata.json
|
||||
• Загрузка обложек
|
||||
|
||||
[grey]Используйте клавиши 1-4 или мышь для навигации[-]`)
|
||||
|
||||
m.infoPanel.SetBorder(true).SetTitle(" Информация ")
|
||||
|
||||
mainMenuFlex := tview.NewFlex().
|
||||
AddItem(m.menuList, 0, 1, true).
|
||||
AddItem(m.infoPanel, 0, 1, false)
|
||||
|
||||
m.pages.AddPage("main", mainMenuFlex, true, true)
|
||||
}
|
||||
|
||||
// setupProcessingScreen настраивает экран обработки
|
||||
func (m *Manager) setupProcessingScreen() {
|
||||
m.logView = tview.NewTextView().
|
||||
SetDynamicColors(true).
|
||||
SetScrollable(true).
|
||||
SetWrap(true)
|
||||
m.logView.SetBorder(true).SetTitle(" Лог обработки ")
|
||||
|
||||
m.progressBar = tview.NewTextView().
|
||||
SetDynamicColors(true).
|
||||
SetTextAlign(tview.AlignCenter)
|
||||
m.progressBar.SetBorder(true).SetTitle(" Прогресс ")
|
||||
|
||||
m.progressText = tview.NewTextView().
|
||||
SetDynamicColors(true).
|
||||
SetTextAlign(tview.AlignCenter).
|
||||
SetText("[white]Ожидание начала обработки...[-]")
|
||||
|
||||
progressFlex := tview.NewFlex().
|
||||
SetDirection(tview.FlexRow).
|
||||
AddItem(m.progressBar, 3, 0, false).
|
||||
AddItem(m.progressText, 1, 0, false)
|
||||
|
||||
processingFlex := tview.NewFlex().
|
||||
SetDirection(tview.FlexRow).
|
||||
AddItem(progressFlex, 4, 0, false).
|
||||
AddItem(m.logView, 0, 1, true)
|
||||
|
||||
m.pages.AddPage("processing", processingFlex, true, false)
|
||||
}
|
||||
|
||||
// setupSettingsScreen настраивает экран настроек
|
||||
func (m *Manager) setupSettingsScreen() {
|
||||
m.settingsForm = tview.NewForm().
|
||||
AddInputField("Исходная директория:", "", 50, nil, nil).
|
||||
AddInputField("Целевая директория:", "", 50, nil, nil).
|
||||
AddInputField("Имя пользователя RuTracker:", "", 30, nil, nil).
|
||||
AddPasswordField("Пароль RuTracker:", "", 30, '*', nil).
|
||||
AddDropDown("Уровень логирования:", []string{"info", "debug"}, 0, nil).
|
||||
AddButton("Сохранить", func() {
|
||||
// TODO: Сохранить настройки
|
||||
}).
|
||||
AddButton("Отмена", func() {
|
||||
m.switchToMain()
|
||||
})
|
||||
|
||||
m.settingsForm.SetBorder(true).SetTitle(" Настройки ")
|
||||
m.pages.AddPage("settings", m.settingsForm, true, false)
|
||||
}
|
||||
|
||||
// setupResultsScreen настраивает экран результатов
|
||||
func (m *Manager) setupResultsScreen() {
|
||||
m.resultsTable = tview.NewTable().
|
||||
SetSelectable(true, false)
|
||||
m.resultsTable.SetBorder(true).SetTitle(" Обработанные аудиокниги ")
|
||||
|
||||
m.detailView = tview.NewTextView().
|
||||
SetDynamicColors(true).
|
||||
SetWrap(true)
|
||||
m.detailView.SetBorder(true).SetTitle(" Детали ")
|
||||
|
||||
resultsFlex := tview.NewFlex().
|
||||
AddItem(m.resultsTable, 0, 2, true).
|
||||
AddItem(m.detailView, 0, 1, false)
|
||||
|
||||
m.pages.AddPage("results", resultsFlex, true, false)
|
||||
}
|
||||
|
||||
// setupPages настраивает систему страниц
|
||||
func (m *Manager) setupPages() {
|
||||
m.app.SetRoot(m.mainFlex, true)
|
||||
|
||||
// Глобальные горячие клавиши
|
||||
m.app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
switch event.Key() {
|
||||
case tcell.KeyEscape:
|
||||
m.switchToMain()
|
||||
return nil
|
||||
case tcell.KeyF1:
|
||||
m.switchToMain()
|
||||
return nil
|
||||
case tcell.KeyF2:
|
||||
m.switchToProcessing()
|
||||
return nil
|
||||
case tcell.KeyF3:
|
||||
m.switchToSettings()
|
||||
return nil
|
||||
case tcell.KeyF4:
|
||||
m.switchToResults()
|
||||
return nil
|
||||
}
|
||||
return event
|
||||
})
|
||||
}
|
||||
|
||||
// handleUpdates обрабатывает обновления в горутине
|
||||
func (m *Manager) handleUpdates() {
|
||||
for {
|
||||
select {
|
||||
case status := <-m.statusUpdate:
|
||||
m.app.QueueUpdateDraw(func() {
|
||||
m.updateProgress(status)
|
||||
})
|
||||
case logMsg := <-m.logUpdate:
|
||||
m.app.QueueUpdateDraw(func() {
|
||||
m.addLogMessage(logMsg)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// updateProgress обновляет прогресс
|
||||
func (m *Manager) updateProgress(status entities.ProcessingStatus) {
|
||||
if status.Total > 0 {
|
||||
progress := float64(status.Current) / float64(status.Total) * 100
|
||||
m.progressBar.SetText(fmt.Sprintf("[green]%.1f%% (%d/%d)[-]", progress, status.Current, status.Total))
|
||||
}
|
||||
|
||||
if status.Error != nil {
|
||||
m.progressText.SetText(fmt.Sprintf("[red]Ошибка: %s[-]", status.Error.Error()))
|
||||
} else {
|
||||
m.progressText.SetText(fmt.Sprintf("[white]%s[-]", status.Status))
|
||||
}
|
||||
}
|
||||
|
||||
// addLogMessage добавляет сообщение в лог
|
||||
func (m *Manager) addLogMessage(message string) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
m.logBuffer = append(m.logBuffer, message)
|
||||
if len(m.logBuffer) > m.maxLogLines {
|
||||
m.logBuffer = m.logBuffer[1:]
|
||||
}
|
||||
|
||||
m.logView.SetText(strings.Join(m.logBuffer, "\n"))
|
||||
m.logView.ScrollToEnd()
|
||||
}
|
||||
|
||||
// Методы переключения страниц
|
||||
func (m *Manager) switchToMain() {
|
||||
m.currentScreen = entities.ScreenMainMenu
|
||||
m.pages.SwitchToPage("main")
|
||||
m.app.SetFocus(m.menuList)
|
||||
}
|
||||
|
||||
func (m *Manager) switchToProcessing() {
|
||||
m.currentScreen = entities.ScreenProcessing
|
||||
m.pages.SwitchToPage("processing")
|
||||
m.app.SetFocus(m.logView)
|
||||
}
|
||||
|
||||
func (m *Manager) switchToSettings() {
|
||||
m.currentScreen = entities.ScreenSettings
|
||||
m.pages.SwitchToPage("settings")
|
||||
m.app.SetFocus(m.settingsForm)
|
||||
}
|
||||
|
||||
func (m *Manager) switchToResults() {
|
||||
m.currentScreen = entities.ScreenResults
|
||||
m.pages.SwitchToPage("results")
|
||||
m.app.SetFocus(m.resultsTable)
|
||||
}
|
||||
Reference in New Issue
Block a user