package tui import ( "fmt" "os" "path/filepath" "strings" "time" "gitgud.io/mike/mpv-manager/internal/assets" "gitgud.io/mike/mpv-manager/pkg/config" "gitgud.io/mike/mpv-manager/pkg/constants" "gitgud.io/mike/mpv-manager/pkg/locale" "gitgud.io/mike/mpv-manager/pkg/log" "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ) var whiteStyle = lipgloss.NewStyle().Foreground(white) type langPrefTypeItem struct { prefType string isAudio bool } func (l langPrefTypeItem) Title() string { if l.isAudio { current := config.GetConfigValue(constants.ConfigKeyAudioLanguage) title := "πŸ”Š Audio Language Preference" if len(current) > 0 { title += fmt.Sprintf(" (currently: %s)", strings.Join(current, ", ")) } return title } else { current := config.GetConfigValue(constants.ConfigKeySubtitleLanguage) title := "πŸ’¬ Subtitle Language Preference" if len(current) > 0 { title += fmt.Sprintf(" (currently: %s)", strings.Join(current, ", ")) } return title } } func (l langPrefTypeItem) Description() string { if l.isAudio { return "Configure audio track language priority" } return "Configure subtitle track language priority" } func (l langPrefTypeItem) FilterValue() string { return l.prefType } type majorLangItem struct { entry *locale.LocaleEntry } func (m majorLangItem) Title() string { flag := locale.GetFlagForLanguageCode(m.entry.LanguageCode) return fmt.Sprintf("%s %s - %s (%s)", flag, m.entry.LanguageName, m.entry.LanguageLocal, m.entry.LanguageCode) } func (m majorLangItem) Description() string { return fmt.Sprintf("Code: %s", m.entry.LanguageCode) } func (m majorLangItem) FilterValue() string { return fmt.Sprintf("%s %s %s", m.entry.LanguageName, m.entry.LanguageLocal, m.entry.LanguageCode) } type regionLangItem struct { entry *locale.LocaleEntry region *locale.RegionEntry isMajor bool } func (r regionLangItem) Title() string { if r.isMajor { return fmt.Sprintf("Use %s only (%s)", r.entry.LanguageName, r.entry.LanguageCode) } // Format: "πŸ‡ΈπŸ‡¦ Arabic - Saudi Arabia β˜…" (with star for major variants) countryPart := fmt.Sprintf("%s", r.region.CountryName) if r.region.MajorRegionalVariant { countryPart += " β˜…" } return fmt.Sprintf("%s %s - %s", r.region.Flag, r.entry.LanguageName, countryPart) } func (r regionLangItem) Description() string { if r.isMajor { return "Priority #1: Major language (no regional preference)" } // Format: "Ψ§Ω„ΨΉΨ±Ψ¨ΩŠΨ© - Ψ§Ω„Ω…Ω…Ω„ΩƒΨ© Ψ§Ω„ΨΉΨ±Ψ¨ΩŠΨ© Ψ§Ω„Ψ³ΨΉΩˆΨ―ΩŠΨ© / Al-Β΄Arabiya as-SaΒ΄ (ar-SA)" // Note: CountryLocal already contains the transliteration separated by " / " return fmt.Sprintf("%s - %s (%s)", r.entry.LanguageLocal, r.region.CountryLocal, r.region.Locale) } func (r regionLangItem) FilterValue() string { if r.isMajor { return "major only" } return fmt.Sprintf("%s %s %s", r.region.CountryName, r.region.CountryLocal, r.region.Locale) } type selectedLangItem struct { index int locale string display string isAdd bool isRemove bool isConfirm bool } func (s selectedLangItem) Title() string { return s.display } func (s selectedLangItem) Description() string { if s.isAdd { return "Add a language to your priority list" } if s.isRemove { return "Remove selected language from list" } return "" } func (s selectedLangItem) FilterValue() string { return s.display } func (m *Model) setupLangPreferenceList() { items := []list.Item{ langPrefTypeItem{prefType: "audio", isAudio: true}, langPrefTypeItem{prefType: "subtitle", isAudio: false}, } m.langPreferenceList = m.createStyledList("Language Preferences", items) } func (m *Model) loadLanguageData() { locales, err := assets.ReadLocales() if err != nil { log.Warn(fmt.Sprintf("Failed to load locales: %v", err)) m.loadedLocales = []locale.LocaleEntry{} return } m.loadedLocales = locales m.loadCurrentMPVLanguageValues() } func (m *Model) loadCurrentMPVLanguageValues() { m.currentMPVAudioLangs = config.GetConfigValue(constants.ConfigKeyAudioLanguage) m.currentMPVSubtitleLangs = config.GetConfigValue(constants.ConfigKeySubtitleLanguage) if len(m.currentMPVAudioLangs) == 0 { m.currentMPVAudioLangs = []string{} } if len(m.currentMPVSubtitleLangs) == 0 { m.currentMPVSubtitleLangs = []string{} } } func (m *Model) handleLangPreferenceUpdate(msg tea.Msg) (*Model, tea.Cmd) { var cmd tea.Cmd m.langPreferenceList, cmd = m.langPreferenceList.Update(msg) switch msg := msg.(type) { case tea.KeyMsg: switch msg.Type { case tea.KeyEsc: m.state = StateMainMenu return m, nil case tea.KeyEnter: selected := m.langPreferenceList.SelectedItem() if item, ok := selected.(langPrefTypeItem); ok { m.langPreferenceType = item.prefType if m.langPreferenceType == "audio" { m.selectedLanguageCodes = make([]string, len(m.currentMPVAudioLangs)) copy(m.selectedLanguageCodes, m.currentMPVAudioLangs) } else { m.selectedLanguageCodes = make([]string, len(m.currentMPVSubtitleLangs)) copy(m.selectedLanguageCodes, m.currentMPVSubtitleLangs) } m.state = StateLanguageSelectedList m.setupSelectedLangList() } } } return m, cmd } func (m *Model) handleMajorLangUpdate(msg tea.Msg) (*Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: // Handle search mode if m.isLangSearching { switch msg.Type { case tea.KeyEsc: // If search term is not empty, clear it but stay in search mode if m.langSearchTerm != "" { m.langSearchTerm = "" m.setupMajorLangList() return m, nil } // If search term is empty, exit search mode m.clearLanguageSearch() m.setupMajorLangList() return m, nil case tea.KeyEnter: // Select the currently highlighted item from search results selected := m.majorLangList.SelectedItem() if selected != nil { // Handle both majorLangItem and regionLangItem (from search results) switch item := selected.(type) { case majorLangItem: m.currentMajorLanguage = item.entry m.majorLangList.ResetFilter() m.clearLanguageSearch() if len(item.entry.Regions) == 0 { m.selectedLanguageCodes = append(m.selectedLanguageCodes, item.entry.LanguageCode) m.saveLanguagePreferences() m.state = StateLanguageSelectedList m.setupSelectedLangList() m.selectedLangList.Select(0) } else { m.state = StateLanguageRegionSelect m.setupRegionLangList() } case regionLangItem: m.majorLangList.ResetFilter() m.clearLanguageSearch() // Directly add regional variant, skip region selection screen var localeCode string if item.isMajor { localeCode = item.entry.LanguageCode } else { localeCode = item.region.Locale } m.selectedLanguageCodes = append(m.selectedLanguageCodes, localeCode) m.saveLanguagePreferences() m.state = StateLanguageSelectedList m.setupSelectedLangList() m.selectedLangList.Select(0) } } return m, nil case tea.KeyBackspace: if len(m.langSearchTerm) > 0 { m.langSearchTerm = m.langSearchTerm[:len(m.langSearchTerm)-1] m.setupMajorLangList() } return m, nil case tea.KeyRunes: if len(msg.Runes) > 0 { m.langSearchTerm += string(msg.Runes) m.setupMajorLangList() } return m, nil } // Pass through up/down arrows to navigate within search results // Don't return yet, let the list update happen below } // Handle normal mode switch msg.Type { case tea.KeyEsc: if m.majorLangList.SettingFilter() || m.majorLangList.IsFiltered() { m.majorLangList.ResetFilter() return m, nil } m.state = StateLanguageSelectedList m.setupSelectedLangList() return m, nil case tea.KeyRunes: // Press '/' to enter search mode if len(msg.Runes) == 1 && msg.Runes[0] == '/' { m.isLangSearching = true m.langSearchTerm = "" return m, nil } } } var cmd tea.Cmd // Allow up/down navigation even in search mode m.majorLangList, cmd = m.majorLangList.Update(msg) // Handle Enter key (for normal mode, not search mode) // Search mode Enter is handled above in the first switch statement if keyMsg, ok := msg.(tea.KeyMsg); ok && keyMsg.Type == tea.KeyEnter && !m.isLangSearching { selected := m.majorLangList.SelectedItem() // Handle both majorLangItem and regionLangItem (from search results) switch item := selected.(type) { case majorLangItem: m.currentMajorLanguage = item.entry m.majorLangList.ResetFilter() m.clearLanguageSearch() if len(item.entry.Regions) == 0 { m.selectedLanguageCodes = append(m.selectedLanguageCodes, item.entry.LanguageCode) m.saveLanguagePreferences() m.state = StateLanguageSelectedList m.setupSelectedLangList() m.selectedLangList.Select(0) } else { m.state = StateLanguageRegionSelect m.setupRegionLangList() } case regionLangItem: m.majorLangList.ResetFilter() m.clearLanguageSearch() // Directly add regional variant, skip region selection screen var localeCode string if item.isMajor { localeCode = item.entry.LanguageCode } else { localeCode = item.region.Locale } m.selectedLanguageCodes = append(m.selectedLanguageCodes, localeCode) m.saveLanguagePreferences() m.state = StateLanguageSelectedList m.setupSelectedLangList() m.selectedLangList.Select(0) } } return m, cmd } func (m *Model) handleRegionLangUpdate(msg tea.Msg) (*Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: // Handle search mode if m.isLangSearching { switch msg.Type { case tea.KeyEsc: // If search term is not empty, clear it but stay in search mode if m.langSearchTerm != "" { m.langSearchTerm = "" m.setupRegionLangList() return m, nil } // If search term is empty, exit search mode m.clearLanguageSearch() m.setupRegionLangList() return m, nil case tea.KeyEnter: // Select the currently highlighted item from search results selected := m.regionLangList.SelectedItem() if selected != nil { if item, ok := selected.(regionLangItem); ok { m.regionLangList.ResetFilter() m.clearLanguageSearch() var localeCode string if item.isMajor { localeCode = item.entry.LanguageCode } else { localeCode = item.region.Locale } m.selectedLanguageCodes = append(m.selectedLanguageCodes, localeCode) m.saveLanguagePreferences() m.state = StateLanguageSelectedList m.setupSelectedLangList() m.selectedLangList.Select(0) } } return m, nil case tea.KeyBackspace: if len(m.langSearchTerm) > 0 { m.langSearchTerm = m.langSearchTerm[:len(m.langSearchTerm)-1] m.setupRegionLangList() } return m, nil case tea.KeyRunes: if len(msg.Runes) > 0 { m.langSearchTerm += string(msg.Runes) m.setupRegionLangList() } return m, nil } // Pass through up/down arrows to navigate within search results // Don't return yet, let the list update happen below } // Handle normal mode switch msg.Type { case tea.KeyEsc: if m.regionLangList.SettingFilter() || m.regionLangList.IsFiltered() { m.regionLangList.ResetFilter() return m, nil } m.state = StateLanguageMajorSelect m.clearLanguageSearch() m.setupMajorLangList() return m, nil case tea.KeyRunes: // Press '/' to enter search mode if len(msg.Runes) == 1 && msg.Runes[0] == '/' { m.isLangSearching = true m.langSearchTerm = "" return m, nil } } } var cmd tea.Cmd // Allow up/down navigation even in search mode m.regionLangList, cmd = m.regionLangList.Update(msg) // Handle Enter key (for normal mode, not search mode) // Search mode Enter is handled above in the first switch statement if keyMsg, ok := msg.(tea.KeyMsg); ok && keyMsg.Type == tea.KeyEnter && !m.isLangSearching { selected := m.regionLangList.SelectedItem() if item, ok := selected.(regionLangItem); ok { m.regionLangList.ResetFilter() m.clearLanguageSearch() var localeCode string if item.isMajor { localeCode = item.entry.LanguageCode } else { localeCode = item.region.Locale } m.selectedLanguageCodes = append(m.selectedLanguageCodes, localeCode) m.saveLanguagePreferences() m.state = StateLanguageSelectedList m.setupSelectedLangList() m.selectedLangList.Select(0) } } return m, cmd } func (m *Model) handleSelectedLangUpdate(msg tea.Msg) (*Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: switch msg.Type { case tea.KeyEsc: if m.selectedLangList.SettingFilter() || m.selectedLangList.IsFiltered() { m.selectedLangList.ResetFilter() return m, nil } m.state = StateMainMenu return m, nil case tea.KeyRunes: if len(msg.Runes) == 1 && msg.Runes[0] == 'r' { m.removeSelectedLanguage() m.setupSelectedLangList() return m, nil } // Handle priority list reordering with Ctrl+↑/Ctrl+↓ case tea.KeyCtrlUp: // Get current index before moving selected := m.selectedLangList.SelectedItem() if item, ok := selected.(selectedLangItem); ok { currentIndex := item.index // Only move if not a special item and not at top if currentIndex > 0 && !item.isAdd && !item.isRemove && !item.isConfirm { m.moveSelectedLanguageUp() m.setupSelectedLangList() // Restore selection - item moved up in selectedLanguageCodes // List structure: [0: Add, 1-N: languages, N+1: Confirm] // Language index 'currentIndex' in selectedLanguageCodes maps to list index 'currentIndex + 1' // After moving up, item is at 'currentIndex - 1' in selectedLanguageCodes // Which maps to list index '(currentIndex - 1) + 1 = currentIndex' m.selectedLangList.Select(currentIndex) } } return m, nil case tea.KeyCtrlDown: // Get current index before moving selected := m.selectedLangList.SelectedItem() if item, ok := selected.(selectedLangItem); ok { currentIndex := item.index // Only move if not a special item and not at bottom if currentIndex >= 0 && currentIndex < len(m.selectedLanguageCodes)-1 && !item.isAdd && !item.isRemove && !item.isConfirm { m.moveSelectedLanguageDown() m.setupSelectedLangList() // Restore selection - item moved down in selectedLanguageCodes // List structure: [0: Add, 1-N: languages, N+1: Confirm] // Language index 'currentIndex' in selectedLanguageCodes maps to list index 'currentIndex + 1' // After moving down, item is at 'currentIndex + 1' in selectedLanguageCodes // Which maps to list index '(currentIndex + 1) + 1 = currentIndex + 2' m.selectedLangList.Select(currentIndex + 2) } } return m, nil } } var cmd tea.Cmd m.selectedLangList, cmd = m.selectedLangList.Update(msg) switch msg := msg.(type) { case tea.KeyMsg: switch msg.Type { case tea.KeyEnter: selected := m.selectedLangList.SelectedItem() if item, ok := selected.(selectedLangItem); ok { if item.isAdd { m.state = StateLanguageMajorSelect m.setupMajorLangList() } else if item.isRemove { m.removeSelectedLanguage() m.setupSelectedLangList() } else if item.isConfirm { m.applyLanguagePreferences() m.loadCurrentMPVLanguageValues() m.state = StateLanguageConfirm } } case tea.KeyRunes: if len(msg.Runes) == 1 && msg.Runes[0] == 'r' { m.removeSelectedLanguage() m.setupSelectedLangList() } } } return m, cmd } func (m *Model) removeSelectedLanguage() { selected := m.selectedLangList.SelectedItem() if item, ok := selected.(selectedLangItem); ok && !item.isAdd && !item.isRemove && !item.isConfirm { index := item.index if index >= 0 && index < len(m.selectedLanguageCodes) { m.selectedLanguageCodes = append( m.selectedLanguageCodes[:index], m.selectedLanguageCodes[index+1:]...) m.saveLanguagePreferences() m.setupSelectedLangList() m.selectedLangList.Select(0) } } } // moveSelectedLanguageUp moves the selected language up in the priority list func (m *Model) moveSelectedLanguageUp() { selected := m.selectedLangList.SelectedItem() if item, ok := selected.(selectedLangItem); ok { index := item.index // Can only move if not a special item (Add/Remove/Confirm) and not at top if index > 0 && !item.isAdd && !item.isRemove && !item.isConfirm { // Swap with previous item m.selectedLanguageCodes[index-1], m.selectedLanguageCodes[index] = m.selectedLanguageCodes[index], m.selectedLanguageCodes[index-1] // Save to mpv.conf var err error if m.langPreferenceType == "audio" { err = config.SetConfigValue(constants.ConfigKeyAudioLanguage, m.selectedLanguageCodes, true) } else { err = config.SetConfigValue(constants.ConfigKeySubtitleLanguage, m.selectedLanguageCodes, true) } if err != nil { log.Error(fmt.Sprintf("Failed to update language priority: %v", err)) } } } } // moveSelectedLanguageDown moves the selected language down in the priority list func (m *Model) moveSelectedLanguageDown() { selected := m.selectedLangList.SelectedItem() if item, ok := selected.(selectedLangItem); ok { index := item.index // Can only move if not a special item (Add/Remove/Confirm) and not at bottom if index >= 0 && index < len(m.selectedLanguageCodes)-1 && !item.isAdd && !item.isRemove && !item.isConfirm { // Swap with next item m.selectedLanguageCodes[index], m.selectedLanguageCodes[index+1] = m.selectedLanguageCodes[index+1], m.selectedLanguageCodes[index] // Save to mpv.conf var err error if m.langPreferenceType == "audio" { err = config.SetConfigValue(constants.ConfigKeyAudioLanguage, m.selectedLanguageCodes, true) } else { err = config.SetConfigValue(constants.ConfigKeySubtitleLanguage, m.selectedLanguageCodes, true) } if err != nil { log.Error(fmt.Sprintf("Failed to update language priority: %v", err)) } } } } func (m *Model) setupMajorLangList() { delegate := list.NewDefaultDelegate() delegate.ShowDescription = true delegate.Styles.SelectedTitle = selectedItemStyle delegate.Styles.SelectedDesc = infoStyle var items []list.Item // Use search results if in search mode if m.langSearchTerm != "" { opts := locale.LanguageFilterOptions{ SearchTerm: m.langSearchTerm, } searchResults := locale.SearchLanguages(m.loadedLocales, opts) m.langSearchResults = searchResults // Convert search results to regionLangItem to preserve regional info convertedItems := ConvertLanguageOptionsToRegionItems(searchResults, m.loadedLocales) items = make([]list.Item, len(convertedItems)) for i, item := range convertedItems { items[i] = item } } else { // Use major languages only (33 common languages, not all 123+ locales) majorLangs := locale.GetMajorLanguages(m.loadedLocales) items = make([]list.Item, 0, len(majorLangs)+10) // +10 for Spanish (Latin America) and others for _, entry := range majorLangs { items = append(items, majorLangItem{entry: &entry}) // Special case: Add Spanish (Latin America) after Spanish if entry.LanguageCode == "es" && strings.Contains(strings.ToLower(entry.LanguageName), "spanish") { // Find Spanish (Latin America) entry (search case-insensitively and check for variations) for _, latinEntry := range m.loadedLocales { entryLower := strings.ToLower(latinEntry.LanguageName) // Check for multiple possible name variations if latinEntry.LanguageCode == "es" && (entryLower == "spanish (latin america)" || entryLower == "spanish latin america" || entryLower == "latin american spanish" || strings.Contains(entryLower, "latin")) { items = append(items, majorLangItem{entry: &latinEntry}) break } } } } } m.majorLangList = m.createStyledList("Select Major Language", items) } // clearLanguageSearch resets the search state func (m *Model) clearLanguageSearch() { m.langSearchTerm = "" m.isLangSearching = false m.langSearchResults = nil } // applyLanguageSearch updates the search term and refreshes the list func (m *Model) applyLanguageSearch(term string) { m.langSearchTerm = term m.setupMajorLangList() } func (m *Model) setupRegionLangList() { var items []list.Item // Add "major language only" option items = append(items, regionLangItem{ entry: m.currentMajorLanguage, region: nil, isMajor: true, }) // If search term is provided, filter regions by search term if m.langSearchTerm != "" { // Search languages with country filter (use language code from current major language) opts := locale.LanguageFilterOptions{ SearchTerm: m.langSearchTerm, CountryCode: "", // Don't filter by country, allow all regions } searchResults := locale.SearchLanguages(m.loadedLocales, opts) // Filter to only include regions that match the current major language for _, opt := range searchResults { // Extract language code from the search result locale langCode := extractLanguageCode(opt.Locale) if langCode == m.currentMajorLanguage.LanguageCode { // Convert to regionLangItem item := LanguageOptionToRegionLangItem(opt, m.loadedLocales) if !item.isMajor { // Skip the "major language only" entry items = append(items, item) } } } } else { // Use sorted regions (major variants first, then by population) sortedRegions := locale.GetMajorRegionalVariants(m.currentMajorLanguage) for _, region := range sortedRegions { items = append(items, regionLangItem{ entry: m.currentMajorLanguage, region: ®ion, isMajor: false, // Always false for regional variants (only the top "use language only" entry is major) }) } } m.regionLangList = m.createStyledList(fmt.Sprintf("Select %s Regional Variant", m.currentMajorLanguage.LanguageName), items) } func (m *Model) setupSelectedLangList() { delegate := list.NewDefaultDelegate() delegate.ShowDescription = true delegate.Styles.SelectedTitle = selectedItemStyle delegate.Styles.SelectedDesc = infoStyle items := []list.Item{ selectedLangItem{ index: -1, locale: "", display: "+ Select another language", isAdd: true, }, } for i, code := range m.selectedLanguageCodes { var entry *locale.LocaleEntry var region *locale.RegionEntry var flag string var languageName string var nativeName string var displayCode string var display string // Parse the locale code if strings.Contains(code, "-") { // Regional variant (e.g., "ja-JP", "en-US") entry, region = locale.FindByLocale(code, m.loadedLocales) if entry != nil && region != nil { flag = region.Flag if flag == "" { flag = locale.GetFlagForLanguageCode(entry.LanguageCode) } languageName = entry.LanguageName nativeName = entry.LanguageLocal displayCode = region.Locale } else if entry != nil { // Entry found but region is nil (fallback) flag = locale.GetFlagForLanguageCode(entry.LanguageCode) languageName = entry.LanguageName nativeName = entry.LanguageLocal displayCode = code } } else { // Major language only (e.g., "ja", "en") entry = locale.FindByLanguageCode(code, m.loadedLocales) if entry != nil { flag = locale.GetFlagForLanguageCode(entry.LanguageCode) languageName = entry.LanguageName nativeName = entry.LanguageLocal displayCode = entry.LanguageCode } } // Create display string matching selection list format if entry != nil { // Use the same format as selection lists: "πŸ‡―πŸ‡΅ Japanese - ζ—₯本θͺž (にほんご) (ja)" // For priority list, we omit the number prefix to keep it clean display = fmt.Sprintf("%s %s - %s (%s)", flag, languageName, nativeName, displayCode) items = append(items, selectedLangItem{ index: i, locale: code, display: display, isAdd: false, }) } else { // Entry not found (fallback) display = fmt.Sprintf("%s (%s)", code, code) items = append(items, selectedLangItem{ index: i, locale: code, display: display, isAdd: false, }) } } items = append(items, selectedLangItem{ index: -1, locale: "", display: "βœ“ Confirm & Apply to mpv.conf", isConfirm: true, }) m.selectedLangList = m.createStyledList("Language Priority List", items) } func (m *Model) saveLanguagePreferences() { // No longer need to save to JSON - mpv.conf is single source of truth // Language preferences will be applied via applyLanguagePreferences() } func (m *Model) applyLanguagePreferences() { configPath := config.GetConfigPath() // Backup before modification m.backupMPVConfig(configPath) // Apply language preferences var err error if m.langPreferenceType == "audio" { err = config.SetConfigValue(constants.ConfigKeyAudioLanguage, m.selectedLanguageCodes, true) } else { err = config.SetConfigValue(constants.ConfigKeySubtitleLanguage, m.selectedLanguageCodes, true) } if err != nil { log.Error(fmt.Sprintf("Failed to apply language preferences: %v", err)) return } log.Info("Language preferences applied to mpv.conf") } func (m *Model) backupMPVConfig(configPath string) { timestamp := time.Now().Format("2006-01-02") backupPath := filepath.Join(filepath.Dir(configPath), fmt.Sprintf("%s-mpv.conf.bak", timestamp)) if err := os.Rename(configPath, backupPath); err != nil { log.Warn(fmt.Sprintf("Failed to create backup: %v", err)) } else { log.Info(fmt.Sprintf("Backup created: %s", backupPath)) } } func (m Model) langPreferenceSelectView() string { var content strings.Builder content.WriteString(infoStyle.Render("Configure MPV language priority for:")) content.WriteString("\n\n") content.WriteString(m.langPreferenceList.View()) return m.wrapContentInViewport(content.String()) } func (m Model) majorLangSelectViewRaw() string { var content strings.Builder title := "Select Audio Language" if m.langPreferenceType == "subtitle" { title = "Select Subtitle Language" } content.WriteString(titleStyle.Render(title)) content.WriteString("\n") content.WriteString(subtitleStyle.Render("═══════════════════════")) content.WriteString("\n\n") // Show search input if in search mode if m.isLangSearching { content.WriteString(infoStyle.Render("Search: ")) // Yellow/bold search text for better visibility searchTextStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("214")).Bold(true) // Orange/Yellow content.WriteString(searchTextStyle.Render("/" + m.langSearchTerm + "β–ˆ")) content.WriteString("\n") content.WriteString(infoStyle.Render("Type to search, ↑↓ to navigate, Enter to select")) content.WriteString("\n") content.WriteString(infoStyle.Render("Esc to clear search")) content.WriteString("\n\n") } else { // Show normal help text if m.langSearchTerm != "" { // Yellow/bold search term for better visibility searchTextStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("214")).Bold(true) // Orange/Yellow content.WriteString(infoStyle.Render("Search results for: ")) content.WriteString(searchTextStyle.Render(m.langSearchTerm)) if len(m.langSearchResults) > 0 { content.WriteString(infoStyle.Render(fmt.Sprintf(" (%d results found)", len(m.langSearchResults)))) } else { content.WriteString(infoStyle.Render(" (no results)")) } content.WriteString("\n") content.WriteString(infoStyle.Render("Press / to clear search")) content.WriteString("\n") } else { content.WriteString(infoStyle.Render("Press / then type to filter languages")) content.WriteString("\n") } content.WriteString(infoStyle.Render("↑↓ to navigate, Enter to select")) content.WriteString("\n") content.WriteString(infoStyle.Render("Esc to cancel")) content.WriteString("\n\n") } // Always display the list (in both search mode and normal mode) content.WriteString(m.majorLangList.View()) return content.String() } func (m Model) regionLangSelectViewRaw() string { var content strings.Builder title := fmt.Sprintf("Select %s Regional Variant", m.currentMajorLanguage.LanguageName) content.WriteString(titleStyle.Render(title)) content.WriteString("\n") content.WriteString(subtitleStyle.Render("═════════════════════════")) content.WriteString("\n\n") // Show search input if in search mode if m.isLangSearching { content.WriteString(infoStyle.Render("Search: ")) // Yellow/bold search text for better visibility searchTextStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("214")).Bold(true) // Orange/Yellow content.WriteString(searchTextStyle.Render("/" + m.langSearchTerm + "β–ˆ")) content.WriteString("\n") content.WriteString(infoStyle.Render("Type to search, ↑↓ to navigate, Enter to select")) content.WriteString("\n") content.WriteString(infoStyle.Render("Esc to clear search")) content.WriteString("\n\n") } else { // Show normal help text if m.langSearchTerm != "" { // Yellow/bold search term for better visibility searchTextStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("214")).Bold(true) // Orange/Yellow content.WriteString(infoStyle.Render("Search results for: ")) content.WriteString(searchTextStyle.Render(m.langSearchTerm)) content.WriteString("\n") content.WriteString(infoStyle.Render("Press / to clear search")) content.WriteString("\n") } else { content.WriteString(infoStyle.Render("Press / then type to filter regions")) content.WriteString("\n") } content.WriteString(infoStyle.Render("↑↓ to navigate, Enter to select")) content.WriteString("\n") content.WriteString(infoStyle.Render("Esc to go back")) content.WriteString("\n\n") } // Always display the list (in both search mode and normal mode) content.WriteString(m.regionLangList.View()) return content.String() } func (m Model) selectedLangListViewRaw() string { var content strings.Builder content.WriteString(infoStyle.Render("This is your priority list. ")) content.WriteString(infoStyle.Render("MPV will use these languages in order of priority.")) content.WriteString("\n\n") content.WriteString(infoStyle.Render("Select '+ Select another language' at top to add more languages")) content.WriteString("\n") content.WriteString(infoStyle.Render("↑↓ to navigate, Enter to select, 'r' to remove")) content.WriteString("\n") content.WriteString(infoStyle.Render("Ctrl+↑/Ctrl+↓ to reorder priority list")) content.WriteString("\n\n") content.WriteString(m.selectedLangList.View()) return content.String() } func (m Model) languageConfirmView() string { var content strings.Builder title := "Audio Preferences Applied" if m.langPreferenceType == "subtitle" { title = "Subtitle Preferences Applied" } content.WriteString(titleStyle.Render(title)) content.WriteString("\n") content.WriteString(subtitleStyle.Render("═════════════════════════")) content.WriteString("\n\n") content.WriteString(successStyle.Render("βœ“ Language preferences saved")) content.WriteString("\n\n") content.WriteString(infoStyle.Render("MPV configuration updated with:")) content.WriteString("\n") if len(m.selectedLanguageCodes) == 0 { content.WriteString(infoStyle.Render(" (empty - language preference disabled)")) } else { for i, code := range m.selectedLanguageCodes { entry, _ := locale.FindByLocale(code, m.loadedLocales) flag := "" if entry != nil { flag = locale.GetFlagForLanguageCode(entry.LanguageCode) } display := fmt.Sprintf(" %d. %s %s", i+1, flag, code) content.WriteString(infoStyle.Render(display)) content.WriteString("\n") } } content.WriteString("\n") content.WriteString(infoStyle.Render("Press ")) content.WriteString(subtitleStyle.Render("Enter")) content.WriteString(infoStyle.Render(" to return to main menu")) return m.wrapContentInViewport(content.String()) }