1
1

Добавить тесты репозитория файловой системы и реализовать функциональность журналирования файлов.

- Реализовать тесты для поиска MP3-файлов и переименования/организации папок книг в репозитории файловой системы.
- Создать FileLogger для записи сообщений в файл с поддержкой различных уровней журналирования и управления размером файлов.
- Разработать репозиторий RuTracker для обработки поиска торрентов, получения метаданных и загрузки торрент-файлов.
- Добавить тесты для нормализации URL в репозиторий RuTracker.
- Реализовать адаптер логгера TUI для отображения логов в терминальном интерфейсе и, при необходимости, для записи логов в базовый логгер.
- Создать менеджер TUI для управления пользовательским интерфейсом приложения, включая главное меню, экран обработки, настройки и отображение результатов.
This commit is contained in:
Dmitriy Fofanov
2025-09-29 20:40:05 +03:00
parent 49bea780aa
commit 72a66f1664
32 changed files with 4073 additions and 22 deletions

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