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