Достижение: Добавлены скрипты и документация для релиза PDF Compressor.

- Добавлен release-body.md для подробных заметок о релизе на русском языке.
- Реализован release-gitea.ps1 для автоматизированного релиза Gitea с помощью PowerShell.
- Создан release-gitea.sh для автоматизированного релиза Gitea с помощью Bash.
- Добавлен release.sh для сборки и маркировки релизов с поддержкой нескольких платформ.
- Улучшен пользовательский интерфейс благодаря информативному логированию и обработке ошибок.
- Добавлена ​​поддержка переменных окружения и управления конфигурацией.
- Добавлена ​​функция создания архивов и загрузки ресурсов в Gitea.
This commit is contained in:
Dmitriy Fofanov
2025-11-05 09:33:12 +03:00
parent f328d67080
commit ec65cfd05a
43 changed files with 5792 additions and 2 deletions

View File

@@ -0,0 +1,83 @@
package tui
import (
"compressor/internal/domain/repositories"
"fmt"
)
// UILogger адаптер логгера для отображения в UI
type UILogger struct {
fileLogger repositories.Logger
tuiManager *Manager
}
// NewUILogger создает новый UI логгер
func NewUILogger(fileLogger repositories.Logger, tuiManager *Manager) *UILogger {
return &UILogger{
fileLogger: fileLogger,
tuiManager: tuiManager,
}
}
// Debug логирует отладочное сообщение
func (l *UILogger) Debug(format string, args ...interface{}) {
message := fmt.Sprintf(format, args...)
if l.fileLogger != nil {
l.fileLogger.Debug(format, args...)
}
if l.tuiManager != nil {
l.tuiManager.AddLog("DEBUG", message)
}
}
// Info логирует информационное сообщение
func (l *UILogger) Info(format string, args ...interface{}) {
message := fmt.Sprintf(format, args...)
if l.fileLogger != nil {
l.fileLogger.Info(format, args...)
}
if l.tuiManager != nil {
l.tuiManager.AddLog("INFO", message)
}
}
// Warning логирует предупреждение
func (l *UILogger) Warning(format string, args ...interface{}) {
message := fmt.Sprintf(format, args...)
if l.fileLogger != nil {
l.fileLogger.Warning(format, args...)
}
if l.tuiManager != nil {
l.tuiManager.AddLog("WARNING", message)
}
}
// Error логирует ошибку
func (l *UILogger) Error(format string, args ...interface{}) {
message := fmt.Sprintf(format, args...)
if l.fileLogger != nil {
l.fileLogger.Error(format, args...)
}
if l.tuiManager != nil {
l.tuiManager.AddLog("ERROR", message)
}
}
// Success логирует успешное выполнение
func (l *UILogger) Success(format string, args ...interface{}) {
message := fmt.Sprintf(format, args...)
if l.fileLogger != nil {
l.fileLogger.Success(format, args...)
}
if l.tuiManager != nil {
l.tuiManager.AddLog("SUCCESS", message)
}
}
// Close закрывает логгер
func (l *UILogger) Close() error {
if l.fileLogger != nil {
return l.fileLogger.Close()
}
return nil
}

View File

@@ -0,0 +1,786 @@
package tui
import (
"fmt"
"math"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
"compressor/internal/domain/entities"
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
"gopkg.in/yaml.v3"
)
// ConfigData структура для отображения конфигурации в UI
type ConfigData struct {
Scanner struct {
SourceDirectory string `yaml:"source_directory"`
TargetDirectory string `yaml:"target_directory"`
ReplaceOriginal bool `yaml:"replace_original"`
} `yaml:"scanner"`
Compression struct {
Level int `yaml:"level"`
Algorithm string `yaml:"algorithm"`
AutoStart bool `yaml:"auto_start"`
UniPDFLicenseKey string `yaml:"unipdf_license_key"`
EnableJPEG bool `yaml:"enable_jpeg"`
EnablePNG bool `yaml:"enable_png"`
JPEGQuality int `yaml:"jpeg_quality"`
PNGQuality int `yaml:"png_quality"`
} `yaml:"compression"`
Processing struct {
ParallelWorkers int `yaml:"parallel_workers"`
TimeoutSeconds int `yaml:"timeout_seconds"`
RetryAttempts int `yaml:"retry_attempts"`
} `yaml:"processing"`
Output struct {
LogLevel string `yaml:"log_level"`
ProgressBar bool `yaml:"progress_bar"`
LogToFile bool `yaml:"log_to_file"`
LogFileName string `yaml:"log_file_name"`
LogMaxSizeMB int `yaml:"log_max_size_mb"`
} `yaml:"output"`
}
// UI Configuration constants
const (
MaxLogBufferSize = 1000
LogFlushInterval = 50 * time.Millisecond
ProgressBarWidth = 40
MaxFileNameLength = 60
MaxFileNameDisplay = 57
ProgressViewHeight = 9
FormItemLicenseIndex = 5
)
// Manager управляет TUI интерфейсом
type Manager struct {
app *tview.Application
pages *tview.Pages
currentScreen entities.UIScreen
// UI компоненты
mainMenu *tview.List
configForm *tview.Form
progressView *tview.TextView
logView *tview.TextView
statusBar *tview.TextView
// Callbacks
onStartProcessing func()
// Состояние
configData ConfigData
logBuffer []string
statusMutex sync.RWMutex
isProcessing bool
// Оптимизированный батчинг логов через канал
logChan chan string
logDone chan struct{}
logMutex sync.Mutex
}
// NewManager создает новый менеджер TUI
func NewManager() *Manager {
m := &Manager{
app: tview.NewApplication(),
pages: tview.NewPages(),
logBuffer: make([]string, 0, MaxLogBufferSize),
logChan: make(chan string, 100), // Buffered channel для батчинга
logDone: make(chan struct{}),
}
// Запускаем горутину обработки логов
go m.logProcessor()
return m
}
// Initialize инициализирует TUI
func (m *Manager) Initialize() {
m.loadConfig()
m.createUI()
m.setupKeyBindings()
}
// Run запускает TUI
func (m *Manager) Run() error {
return m.app.SetRoot(m.pages, true).EnableMouse(true).Run()
}
// SetOnStartProcessing устанавливает callback для начала обработки
func (m *Manager) SetOnStartProcessing(callback func()) {
m.onStartProcessing = callback
}
// SendStatusUpdate отправляет обновление статуса
func (m *Manager) SendStatusUpdate(status entities.ProcessingStatus) {
m.updateProgress(status)
}
// loadConfig загружает конфигурацию
func (m *Manager) loadConfig() {
configPath := "config.yaml"
if _, err := os.Stat(configPath); os.IsNotExist(err) {
// Создаем конфигурацию по умолчанию
m.configData = ConfigData{
Scanner: struct {
SourceDirectory string `yaml:"source_directory"`
TargetDirectory string `yaml:"target_directory"`
ReplaceOriginal bool `yaml:"replace_original"`
}{
SourceDirectory: "./pdfs",
TargetDirectory: "./compressed",
ReplaceOriginal: false,
},
Compression: struct {
Level int `yaml:"level"`
Algorithm string `yaml:"algorithm"`
AutoStart bool `yaml:"auto_start"`
UniPDFLicenseKey string `yaml:"unipdf_license_key"`
EnableJPEG bool `yaml:"enable_jpeg"`
EnablePNG bool `yaml:"enable_png"`
JPEGQuality int `yaml:"jpeg_quality"`
PNGQuality int `yaml:"png_quality"`
}{
Level: 50,
Algorithm: "pdfcpu",
AutoStart: false,
UniPDFLicenseKey: "",
EnableJPEG: false,
EnablePNG: false,
JPEGQuality: 30,
PNGQuality: 25,
},
Processing: struct {
ParallelWorkers int `yaml:"parallel_workers"`
TimeoutSeconds int `yaml:"timeout_seconds"`
RetryAttempts int `yaml:"retry_attempts"`
}{
ParallelWorkers: 2,
TimeoutSeconds: 30,
RetryAttempts: 3,
},
Output: struct {
LogLevel string `yaml:"log_level"`
ProgressBar bool `yaml:"progress_bar"`
LogToFile bool `yaml:"log_to_file"`
LogFileName string `yaml:"log_file_name"`
LogMaxSizeMB int `yaml:"log_max_size_mb"`
}{
LogLevel: "info",
ProgressBar: true,
LogToFile: true,
LogFileName: "compressor.log",
LogMaxSizeMB: 10,
},
}
m.saveConfig()
return
}
data, err := os.ReadFile(configPath)
if err != nil {
return
}
yaml.Unmarshal(data, &m.configData)
}
// saveConfig сохраняет конфигурацию
func (m *Manager) saveConfig() {
data, err := yaml.Marshal(&m.configData)
if err != nil {
return
}
os.WriteFile("config.yaml", data, 0644)
}
// createUI создает пользовательский интерфейс
func (m *Manager) createUI() {
m.createMainMenu()
m.createConfigScreen()
m.createProcessingScreen()
// m.createResultsScreen()
m.pages.AddPage("menu", m.mainMenu, true, true)
m.pages.AddPage("config", m.configForm, true, false)
m.pages.AddPage("processing", m.createProcessingLayout(), true, false)
m.currentScreen = entities.UIScreenMenu
}
// createMainMenu создает главное меню
func (m *Manager) createMainMenu() {
m.mainMenu = tview.NewList().
AddItem("🚀 Запуск алгоритма сжатия", "Начать автоматическое сжатие PDF файлов", '1', func() {
m.startProcessing()
}).
AddItem("⚙️ Конфигурация", "Настроить параметры сжатия и обработки", '2', func() {
m.switchToScreen(entities.UIScreenConfig)
}).
AddItem("❌ Выход", "Закрыть приложение", 'q', func() {
m.Cleanup()
m.app.Stop()
})
m.mainMenu.SetBorder(true).
SetTitle("🔥 Universal File Compressor - Главное меню").
SetTitleAlign(tview.AlignCenter)
// Настраиваем стиль
m.mainMenu.SetSelectedBackgroundColor(tcell.ColorDarkBlue).
SetSelectedTextColor(tcell.ColorWhite).
SetMainTextColor(tcell.ColorWhite).
SetSecondaryTextColor(tcell.ColorGray)
}
// createConfigScreen создает экран конфигурации
func (m *Manager) createConfigScreen() {
m.configForm = tview.NewForm().
AddInputField("Исходная директория", m.configData.Scanner.SourceDirectory, 60, nil, func(text string) {
m.configData.Scanner.SourceDirectory = text
}).
AddInputField("Целевая директория", m.configData.Scanner.TargetDirectory, 60, nil, func(text string) {
m.configData.Scanner.TargetDirectory = text
}).
AddCheckbox("Заменить оригинал", m.configData.Scanner.ReplaceOriginal, func(checked bool) {
m.configData.Scanner.ReplaceOriginal = checked
}).
AddInputField("Уровень сжатия (10-90)", strconv.Itoa(m.configData.Compression.Level), 10, nil, func(text string) {
if level, err := strconv.Atoi(text); err == nil && level >= 10 && level <= 90 {
m.configData.Compression.Level = level
}
}).
AddDropDown("Алгоритм", []string{"pdfcpu", "unipdf"}, func() int {
if m.configData.Compression.Algorithm == "unipdf" {
return 1
}
return 0
}(), func(option string, optionIndex int) {
m.configData.Compression.Algorithm = option
m.updateLicenseFieldVisibility()
}).
AddInputField("Лицензия UniPDF (UNIDOC_LICENSE_API_KEY)", m.configData.Compression.UniPDFLicenseKey, 60, nil, func(text string) {
m.configData.Compression.UniPDFLicenseKey = text
}).
AddCheckbox("Автостарт", m.configData.Compression.AutoStart, func(checked bool) {
m.configData.Compression.AutoStart = checked
}).
AddCheckbox("Сжимать JPEG", m.configData.Compression.EnableJPEG, func(checked bool) {
m.configData.Compression.EnableJPEG = checked
}).
AddDropDown("Качество JPEG (%)", []string{"10", "15", "20", "25", "30", "35", "40", "45", "50"}, func() int {
return (m.configData.Compression.JPEGQuality - 10) / 5
}(), func(option string, optionIndex int) {
if quality, err := strconv.Atoi(option); err == nil {
m.configData.Compression.JPEGQuality = quality
}
}).
AddCheckbox("Сжимать PNG", m.configData.Compression.EnablePNG, func(checked bool) {
m.configData.Compression.EnablePNG = checked
}).
AddDropDown("Качество PNG (%)", []string{"10", "15", "20", "25", "30", "35", "40", "45", "50"}, func() int {
return (m.configData.Compression.PNGQuality - 10) / 5
}(), func(option string, optionIndex int) {
if quality, err := strconv.Atoi(option); err == nil {
m.configData.Compression.PNGQuality = quality
}
}).
AddButton("Сохранить", func() {
m.saveConfig()
m.switchToScreen(entities.UIScreenMenu)
// Позиционируемся на пункте "Конфигурация" (индекс 1)
m.mainMenu.SetCurrentItem(1)
})
m.updateLicenseFieldVisibility()
m.configForm.SetBorder(true).
SetTitle("🔥 Universal File Compressor - Конфигурация (ESC - выйти без сохранения)").
SetTitleAlign(tview.AlignCenter)
// Обработка ESC для выхода без сохранения
m.configForm.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
if event.Key() == tcell.KeyEscape {
// Перезагружаем конфигурацию из файла (отменяем изменения)
m.loadConfig()
m.switchToScreen(entities.UIScreenMenu)
return nil
}
return event
})
}
// createProcessingScreen создает экран обработки
func (m *Manager) createProcessingScreen() {
m.progressView = tview.NewTextView().
SetDynamicColors(true).
SetRegions(true).
SetScrollable(true)
m.progressView.SetBorder(true).
SetTitle("📊 Прогресс обработки").
SetTitleAlign(tview.AlignCenter)
m.logView = tview.NewTextView().
SetDynamicColors(true).
SetScrollable(true).
SetMaxLines(MaxLogBufferSize)
m.logView.SetBorder(true).
SetTitle("📋 Журнал событий").
SetTitleAlign(tview.AlignCenter)
}
// createProcessingLayout создает layout для экрана обработки
func (m *Manager) createProcessingLayout() *tview.Flex {
return tview.NewFlex().
SetDirection(tview.FlexRow).
AddItem(m.logView, 0, 1, false).
AddItem(m.progressView, ProgressViewHeight, 0, false)
}
// setupKeyBindings настраивает горячие клавиши
func (m *Manager) setupKeyBindings() {
m.app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
switch event.Key() {
case tcell.KeyF1:
m.switchToScreen(entities.UIScreenMenu)
return nil
case tcell.KeyF2:
m.switchToScreen(entities.UIScreenConfig)
return nil
case tcell.KeyF3:
if m.isProcessing {
m.switchToScreen(entities.UIScreenProcessing)
}
return nil
case tcell.KeyEscape:
// ESC работает по-разному в зависимости от экрана
if m.currentScreen == entities.UIScreenConfig {
// В конфигурации ESC обрабатывается локально формой
return event
} else if m.currentScreen != entities.UIScreenMenu {
m.switchToScreen(entities.UIScreenMenu)
return nil
}
}
// Обработка числовых клавиш для меню
if m.currentScreen == entities.UIScreenMenu {
switch event.Rune() {
case '1':
m.startProcessing()
return nil
case '2':
m.switchToScreen(entities.UIScreenConfig)
return nil
case 'q', 'Q':
m.Cleanup()
m.app.Stop()
return nil
}
}
return event
})
}
// switchToScreen переключает на указанный экран
func (m *Manager) switchToScreen(screen entities.UIScreen) {
m.statusMutex.Lock()
defer m.statusMutex.Unlock()
m.currentScreen = screen
switch screen {
case entities.UIScreenMenu:
m.pages.SwitchToPage("menu")
case entities.UIScreenConfig:
// При входе в конфигурацию обновляем данные из файла и синхронизируем форму
m.loadConfig()
m.refreshConfigForm()
m.pages.SwitchToPage("config")
case entities.UIScreenProcessing:
m.pages.SwitchToPage("processing")
}
}
// startProcessing начинает обработку
func (m *Manager) startProcessing() {
m.saveConfig()
m.isProcessing = true
m.switchToScreen(entities.UIScreenProcessing)
if m.onStartProcessing != nil {
go m.onStartProcessing()
}
}
// updateProgress обновляет прогресс
func (m *Manager) updateProgress(status entities.ProcessingStatus) {
if m.progressView == nil {
return
}
// Обновляем прогресс-бар
progressBar := m.createProgressBar(status.Progress, ProgressBarWidth)
// Корректное усечение имени файла с учетом UTF-8
displayFile := m.truncateFileName(status.CurrentFile, MaxFileNameLength, MaxFileNameDisplay)
// Формируем текст статуса
var progressText string
// Фаза обработки
phaseText := status.Phase.String()
if status.Message != "" {
phaseText = status.Message
}
progressText = fmt.Sprintf(
"[yellow]⚙️ Фаза:[white] %s\n\n"+
"[yellow]📁 Текущий файл:[white] %s\n",
phaseText,
filepath.Base(displayFile),
)
// Размер текущего файла
if status.CurrentFileSize > 0 {
progressText += fmt.Sprintf("[dim] Размер: %.2f MB[white]\n", float64(status.CurrentFileSize)/1024/1024)
}
// Прогресс-бар
progressText += fmt.Sprintf(
"\n[cyan]📊 Прогресс:[white] %s [cyan]%.1f%%[white]\n\n",
progressBar,
status.Progress,
)
// Статистика файлов
progressText += fmt.Sprintf(
"[green]📈 Статистика файлов:[white]\n"+
" • Всего: [cyan]%d[white]\n"+
" • Обработано: [cyan]%d[white]\n"+
" • Успешно: [green]%d[white]",
status.TotalFiles,
status.ProcessedFiles,
status.SuccessfulFiles,
)
if status.FailedFiles > 0 {
progressText += fmt.Sprintf("\n • Ошибок: [red]%d[white]", status.FailedFiles)
}
if status.SkippedFiles > 0 {
progressText += fmt.Sprintf("\n • Пропущено: [yellow]%d[white]", status.SkippedFiles)
}
// Статистика сжатия
if status.TotalOriginalSize > 0 {
progressText += fmt.Sprintf(
"\n\n[green]💾 Статистика сжатия:[white]\n"+
" • Исходный размер: [cyan]%.2f MB[white]\n"+
" • Сжатый размер: [cyan]%.2f MB[white]\n"+
" • Среднее сжатие: [green]%.1f%%[white]\n"+
" • Сэкономлено: [green]%.2f MB[white]",
float64(status.TotalOriginalSize)/1024/1024,
float64(status.TotalCompressedSize)/1024/1024,
status.AverageCompression,
float64(status.TotalSavedSpace)/1024/1024,
)
}
// Время выполнения
progressText += fmt.Sprintf(
"\n\n[yellow]⏱️ Время:[white]\n"+
" • Прошло: [cyan]%s[white]",
status.FormatElapsedTime(),
)
if !status.IsComplete && status.EstimatedTime > 0 {
progressText += fmt.Sprintf("\n • Осталось: [cyan]~%s[white]", status.FormatEstimatedTime())
}
progressText += "\n\n"
if status.IsComplete {
if status.Error != nil {
progressText += "[red]❌ Обработка завершена с ошибкой![white]\n"
progressText += fmt.Sprintf("[red]Ошибка: %v[white]\n", status.Error)
} else {
progressText += "[green]✅ Обработка успешно завершена![white]\n"
}
progressText += "\n[yellow]F1[white] - Главное меню\n"
progressText += "[yellow]ESC[white] - Главное меню\n"
m.isProcessing = false
} else {
progressText += "[yellow]F1[white] - Главное меню\n"
progressText += "[yellow]ESC[white] - Главное меню\n"
}
if status.Error != nil {
progressText += fmt.Sprintf("\n[red]❌ Ошибка: %v[white]\n", status.Error)
}
// Обновляем UI потокобезопасно через QueueUpdateDraw
m.app.QueueUpdateDraw(func() {
m.progressView.SetText(progressText)
})
}
// truncateFileName корректно усекает имя файла с учетом UTF-8
func (m *Manager) truncateFileName(fileName string, maxLength, truncateAt int) string {
runes := []rune(fileName)
if len(runes) <= maxLength {
return fileName
}
return string(runes[:truncateAt]) + "..."
}
// createProgressBar создает красивый цветной прогресс-бар
func (m *Manager) createProgressBar(progress float64, width int) string {
// Нормализуем значения
if progress < 0 {
progress = 0
} else if progress > 100 {
progress = 100
}
filled := int(math.Round(progress * float64(width) / 100))
if filled > width {
filled = width
}
if filled < 0 {
filled = 0
}
// Разные символы для заполненной и пустой части
const filledChar = "█"
const emptyChar = "░"
// Цвет зависит от прогресса
var color string
switch {
case progress < 25:
color = "red"
case progress < 50:
color = "yellow"
case progress < 75:
color = "blue"
default:
color = "green"
}
filledPart := strings.Repeat(filledChar, filled)
emptyPart := strings.Repeat(emptyChar, width-filled)
return fmt.Sprintf("[%s]%s[gray]%s", color, filledPart, emptyPart)
}
// AddLog добавляет запись в лог через канал (неблокирующе)
func (m *Manager) AddLog(level, message string) {
var color string
switch strings.ToLower(level) {
case "error":
color = "red"
case "warning":
color = "yellow"
case "success":
color = "green"
case "debug":
color = "gray"
default:
color = "white"
}
logLine := fmt.Sprintf("[%s]%s:[white] %s", color, strings.ToUpper(level), message)
// Неблокирующая отправка в канал
select {
case m.logChan <- logLine:
default:
// Если канал переполнен, пропускаем лог (лучше чем блокировка)
}
}
// logProcessor обрабатывает логи в отдельной горутине с батчингом
func (m *Manager) logProcessor() {
ticker := time.NewTicker(LogFlushInterval)
defer ticker.Stop()
batch := make([]string, 0, 50)
for {
select {
case logLine := <-m.logChan:
batch = append(batch, logLine)
// Если накопился достаточный батч, сбрасываем
if len(batch) >= 20 {
m.flushLogBatch(batch)
batch = make([]string, 0, 50)
}
case <-ticker.C:
// Периодический сброс
if len(batch) > 0 {
m.flushLogBatch(batch)
batch = make([]string, 0, 50)
}
case <-m.logDone:
// Финальный сброс при завершении
if len(batch) > 0 {
m.flushLogBatch(batch)
}
return
}
}
}
// flushLogBatch сбрасывает батч логов в UI
func (m *Manager) flushLogBatch(batch []string) {
m.statusMutex.Lock()
m.logBuffer = append(m.logBuffer, batch...)
// Ограничиваем размер буфера
if len(m.logBuffer) > MaxLogBufferSize {
m.logBuffer = m.logBuffer[len(m.logBuffer)-MaxLogBufferSize:]
}
// Создаем копию буфера для UI
logText := strings.Join(m.logBuffer, "\n")
m.statusMutex.Unlock()
// Обновляем UI потокобезопасно
if m.logView != nil {
m.app.QueueUpdateDraw(func() {
if m.logView != nil { // Двойная проверка
m.logView.SetText(logText)
m.logView.ScrollToEnd()
}
})
}
}
// Cleanup освобождает ресурсы менеджера (идемпотентный)
func (m *Manager) Cleanup() {
m.logMutex.Lock()
defer m.logMutex.Unlock()
// Проверяем, что канал еще открыт
select {
case <-m.logDone:
// Канал уже закрыт
return
default:
// Закрываем канал
close(m.logDone)
}
} // updateLicenseFieldVisibility обновляет видимость поля лицензии в зависимости от выбранного алгоритма
func (m *Manager) updateLicenseFieldVisibility() {
if m.configForm == nil {
return
}
// Получаем количество элементов формы
formItemCount := m.configForm.GetFormItemCount()
if formItemCount > FormItemLicenseIndex {
// Получаем поле лицензии
licenseField := m.configForm.GetFormItem(FormItemLicenseIndex)
if m.configData.Compression.Algorithm == "unipdf" {
// Показываем поле лицензии для UniPDF
licenseField.(*tview.InputField).SetTitle("🔑 Лицензия UniPDF (UNIDOC_LICENSE_API_KEY) - ОБЯЗАТЕЛЬНО")
licenseField.(*tview.InputField).SetFieldBackgroundColor(tcell.ColorDarkBlue)
} else {
// Скрываем поле лицензии для PDFCPU
licenseField.(*tview.InputField).SetTitle("Лицензия UniPDF (не требуется для PDFCPU)")
licenseField.(*tview.InputField).SetFieldBackgroundColor(tcell.ColorDarkGray)
}
}
}
// refreshConfigForm синхронизирует значения формы с текущими данными конфигурации
func (m *Manager) refreshConfigForm() {
if m.configForm == nil {
return
}
// 0: Исходная директория (Input)
if item := m.configForm.GetFormItem(0); item != nil {
item.(*tview.InputField).SetText(m.configData.Scanner.SourceDirectory)
}
// 1: Целевая директория (Input)
if item := m.configForm.GetFormItem(1); item != nil {
item.(*tview.InputField).SetText(m.configData.Scanner.TargetDirectory)
}
// 2: Заменить оригинал (Checkbox)
if item := m.configForm.GetFormItem(2); item != nil {
item.(*tview.Checkbox).SetChecked(m.configData.Scanner.ReplaceOriginal)
}
// 3: Уровень сжатия (Input)
if item := m.configForm.GetFormItem(3); item != nil {
item.(*tview.InputField).SetText(strconv.Itoa(m.configData.Compression.Level))
}
// 4: Алгоритм (DropDown)
if item := m.configForm.GetFormItem(4); item != nil {
dd := item.(*tview.DropDown)
if m.configData.Compression.Algorithm == "unipdf" {
dd.SetCurrentOption(1)
} else {
dd.SetCurrentOption(0)
}
}
// 5: Лицензия UniPDF (Input)
if item := m.configForm.GetFormItem(5); item != nil {
item.(*tview.InputField).SetText(m.configData.Compression.UniPDFLicenseKey)
}
// 6: Автостарт (Checkbox)
if item := m.configForm.GetFormItem(6); item != nil {
item.(*tview.Checkbox).SetChecked(m.configData.Compression.AutoStart)
}
m.updateLicenseFieldVisibility()
}
// GetConfig возвращает текущую конфигурацию в формате entities.Config
func (m *Manager) GetConfig() *entities.Config {
return &entities.Config{
Scanner: entities.ScannerConfig{
SourceDirectory: m.configData.Scanner.SourceDirectory,
TargetDirectory: m.configData.Scanner.TargetDirectory,
ReplaceOriginal: m.configData.Scanner.ReplaceOriginal,
},
Compression: entities.AppCompressionConfig{
Level: m.configData.Compression.Level,
Algorithm: m.configData.Compression.Algorithm,
AutoStart: m.configData.Compression.AutoStart,
UniPDFLicenseKey: m.configData.Compression.UniPDFLicenseKey,
EnableJPEG: m.configData.Compression.EnableJPEG,
EnablePNG: m.configData.Compression.EnablePNG,
JPEGQuality: m.configData.Compression.JPEGQuality,
PNGQuality: m.configData.Compression.PNGQuality,
},
Processing: entities.ProcessingConfig{
ParallelWorkers: m.configData.Processing.ParallelWorkers,
TimeoutSeconds: m.configData.Processing.TimeoutSeconds,
RetryAttempts: m.configData.Processing.RetryAttempts,
},
Output: entities.OutputConfig{
LogLevel: m.configData.Output.LogLevel,
ProgressBar: m.configData.Output.ProgressBar,
LogToFile: m.configData.Output.LogToFile,
LogFileName: m.configData.Output.LogFileName,
LogMaxSizeMB: m.configData.Output.LogMaxSizeMB,
},
}
}