//! Application state module. //! //! Contains `App` — the central state struct parameterized by `TdClientTrait` //! for dependency injection. Methods are organized into trait modules in `methods/`. mod chat_filter; mod chat_state; mod state; pub mod methods; pub use chat_filter::{ChatFilter, ChatFilterCriteria}; pub use chat_state::{ChatState, InputMode}; pub use state::AppScreen; pub use methods::*; use crate::tdlib::{ChatInfo, TdClient, TdClientTrait}; use crate::types::ChatId; use ratatui::widgets::ListState; /// Main application state for the Telegram TUI client. /// /// Manages all application state including authentication, chats, messages, /// and UI state. Integrates with TDLib через `TdClient` and handles user input. /// /// # State Machine /// /// The app uses a type-safe state machine (`ChatState`) for chat-related operations: /// - `Normal` - default state /// - `MessageSelection` - selecting a message /// - `Editing` - editing a message /// - `Reply` - replying to a message /// - `Forward` - forwarding a message /// - `DeleteConfirmation` - confirming deletion /// - `ReactionPicker` - choosing a reaction /// - `Profile` - viewing profile /// - `SearchInChat` - searching within chat /// - `PinnedMessages` - viewing pinned messages /// /// # Examples /// /// ```no_run /// use tele_tui::app::App; /// use tele_tui::app::methods::navigation::NavigationMethods; /// use tele_tui::config::Config; /// /// let config = Config::default(); /// let mut app = App::new(config); /// /// // Navigate through chats /// app.next_chat(); /// app.previous_chat(); /// /// // Open a chat /// app.select_current_chat(); /// ``` pub struct App { // Core (config - readonly через getter) config: crate::config::Config, pub screen: AppScreen, pub td_client: T, /// Состояние чата - type-safe state machine (новое!) pub chat_state: ChatState, /// Vim-like input mode: Normal (navigation) / Insert (text input) pub input_mode: InputMode, // Auth state (приватные, доступ через геттеры) phone_input: String, code_input: String, password_input: String, pub error_message: Option, pub status_message: Option, // Main app state (используются часто) pub chats: Vec, pub chat_list_state: ListState, pub selected_chat_id: Option, pub message_input: String, /// Позиция курсора в message_input (в символах) pub cursor_position: usize, pub message_scroll_offset: usize, /// None = All (основной список), Some(id) = папка с id pub selected_folder_id: Option, pub is_loading: bool, // Search state pub is_searching: bool, pub search_query: String, /// Флаг для оптимизации рендеринга - перерисовывать только при изменениях pub needs_redraw: bool, // Typing indicator /// Время последней отправки typing status (для throttling) pub last_typing_sent: Option, // Image support #[cfg(feature = "images")] pub image_cache: Option, /// Renderer для inline preview в чате (Halfblocks - быстро) #[cfg(feature = "images")] pub inline_image_renderer: Option, /// Renderer для modal просмотра (iTerm2/Sixel - высокое качество) #[cfg(feature = "images")] pub modal_image_renderer: Option, /// Состояние модального окна просмотра изображения #[cfg(feature = "images")] pub image_modal: Option, /// Время последнего рендеринга изображений (для throttling до 15 FPS) #[cfg(feature = "images")] pub last_image_render_time: Option, // Voice playback /// Аудиопроигрыватель для голосовых сообщений (rodio) pub audio_player: Option, /// Кэш голосовых файлов (LRU, max 100 MB) pub voice_cache: Option, /// Состояние текущего воспроизведения pub playback_state: Option, /// Время последнего тика для обновления позиции воспроизведения pub last_playback_tick: Option, } impl App { /// Creates a new App instance with the given configuration and client. /// /// Sets up empty chat list and configures the app to start on the Loading screen. /// /// # Arguments /// /// * `config` - Application configuration loaded from config.toml /// * `td_client` - TDLib client instance (real or fake for tests) /// /// # Returns /// /// A new `App` instance ready to start authentication. pub fn with_client(config: crate::config::Config, td_client: T) -> App { let mut state = ListState::default(); state.select(Some(0)); let audio_cache_size_mb = config.audio.cache_size_mb; #[cfg(feature = "images")] let image_cache = Some(crate::media::cache::ImageCache::new( config.images.cache_size_mb, )); #[cfg(feature = "images")] let inline_image_renderer = crate::media::image_renderer::ImageRenderer::new_fast(); #[cfg(feature = "images")] let modal_image_renderer = crate::media::image_renderer::ImageRenderer::new(); App { config, screen: AppScreen::Loading, td_client, chat_state: ChatState::Normal, input_mode: InputMode::Normal, phone_input: String::new(), code_input: String::new(), password_input: String::new(), error_message: None, status_message: Some("Инициализация TDLib...".to_string()), chats: Vec::new(), chat_list_state: state, selected_chat_id: None, message_input: String::new(), cursor_position: 0, message_scroll_offset: 0, selected_folder_id: None, // None = All is_loading: true, is_searching: false, search_query: String::new(), needs_redraw: true, last_typing_sent: None, #[cfg(feature = "images")] image_cache, #[cfg(feature = "images")] inline_image_renderer, #[cfg(feature = "images")] modal_image_renderer, #[cfg(feature = "images")] image_modal: None, #[cfg(feature = "images")] last_image_render_time: None, // Voice playback audio_player: crate::audio::AudioPlayer::new().ok(), voice_cache: crate::audio::VoiceCache::new(audio_cache_size_mb).ok(), playback_state: None, last_playback_tick: None, } } /// Получить команду из KeyEvent используя настроенные keybindings. /// /// # Arguments /// /// * `key` - KeyEvent от пользователя /// /// # Returns /// /// `Some(Command)` если найдена команда для этой клавиши, `None` если нет pub fn get_command(&self, key: crossterm::event::KeyEvent) -> Option { self.config.keybindings.get_command(&key) } /// Get the selected chat ID as i64 pub fn get_selected_chat_id(&self) -> Option { self.selected_chat_id.map(|id| id.as_i64()) } /// Останавливает воспроизведение голосового и сбрасывает состояние pub fn stop_playback(&mut self) { if let Some(ref player) = self.audio_player { player.stop(); } self.playback_state = None; self.last_playback_tick = None; self.status_message = None; } /// Get the selected chat info pub fn get_selected_chat(&self) -> Option<&ChatInfo> { self.selected_chat_id .and_then(|id| self.chats.iter().find(|c| c.id == id)) } // ========== Getter/Setter методы для инкапсуляции ========== // Config pub fn config(&self) -> &crate::config::Config { &self.config } // Screen pub fn screen(&self) -> &AppScreen { &self.screen } pub fn set_screen(&mut self, screen: AppScreen) { self.screen = screen; } // Auth state pub fn phone_input(&self) -> &str { &self.phone_input } pub fn phone_input_mut(&mut self) -> &mut String { &mut self.phone_input } pub fn set_phone_input(&mut self, input: String) { self.phone_input = input; } pub fn code_input(&self) -> &str { &self.code_input } pub fn code_input_mut(&mut self) -> &mut String { &mut self.code_input } pub fn set_code_input(&mut self, input: String) { self.code_input = input; } pub fn password_input(&self) -> &str { &self.password_input } pub fn password_input_mut(&mut self) -> &mut String { &mut self.password_input } pub fn set_password_input(&mut self, input: String) { self.password_input = input; } pub fn error_message(&self) -> Option<&str> { self.error_message.as_deref() } pub fn set_error_message(&mut self, message: Option) { self.error_message = message; } pub fn status_message(&self) -> Option<&str> { self.status_message.as_deref() } pub fn set_status_message(&mut self, message: Option) { self.status_message = message; } // Main app state pub fn chats(&self) -> &[ChatInfo] { &self.chats } pub fn chats_mut(&mut self) -> &mut Vec { &mut self.chats } pub fn set_chats(&mut self, chats: Vec) { self.chats = chats; } pub fn chat_list_state(&self) -> &ListState { &self.chat_list_state } pub fn chat_list_state_mut(&mut self) -> &mut ListState { &mut self.chat_list_state } pub fn selected_chat_id(&self) -> Option { self.selected_chat_id } pub fn set_selected_chat_id(&mut self, id: Option) { self.selected_chat_id = id; } pub fn message_input(&self) -> &str { &self.message_input } pub fn message_input_mut(&mut self) -> &mut String { &mut self.message_input } pub fn set_message_input(&mut self, input: String) { self.message_input = input; } pub fn cursor_position(&self) -> usize { self.cursor_position } pub fn set_cursor_position(&mut self, pos: usize) { self.cursor_position = pos; } pub fn message_scroll_offset(&self) -> usize { self.message_scroll_offset } pub fn set_message_scroll_offset(&mut self, offset: usize) { self.message_scroll_offset = offset; } pub fn selected_folder_id(&self) -> Option { self.selected_folder_id } pub fn set_selected_folder_id(&mut self, id: Option) { self.selected_folder_id = id; } pub fn is_loading(&self) -> bool { self.is_loading } pub fn set_loading(&mut self, loading: bool) { self.is_loading = loading; } // Search state pub fn is_searching(&self) -> bool { self.is_searching } pub fn set_searching(&mut self, searching: bool) { self.is_searching = searching; } pub fn search_query(&self) -> &str { &self.search_query } pub fn search_query_mut(&mut self) -> &mut String { &mut self.search_query } pub fn set_search_query(&mut self, query: String) { self.search_query = query; } // Redraw flag pub fn needs_redraw(&self) -> bool { self.needs_redraw } pub fn set_needs_redraw(&mut self, redraw: bool) { self.needs_redraw = redraw; } pub fn mark_for_redraw(&mut self) { self.needs_redraw = true; } // Typing indicator pub fn last_typing_sent(&self) -> Option { self.last_typing_sent } pub fn set_last_typing_sent(&mut self, time: Option) { self.last_typing_sent = time; } pub fn update_last_typing_sent(&mut self) { self.last_typing_sent = Some(std::time::Instant::now()); } } // Convenience constructor for real TdClient (production use) impl App { /// Creates a new App instance with the given configuration and a real TDLib client. /// /// This is a convenience method for production use that automatically creates /// a new TdClient instance. /// /// # Arguments /// /// * `config` - Application configuration loaded from config.toml /// /// # Returns /// /// A new `App` instance ready to start authentication. pub fn new(config: crate::config::Config) -> App { App::with_client(config, TdClient::new()) } }