style: auto-format entire codebase with cargo fmt (stable rustfmt.toml)
Some checks failed
ci/woodpecker/pr/check Pipeline failed
CI / Check (pull_request) Has been cancelled
CI / Format (pull_request) Has been cancelled
CI / Clippy (pull_request) Has been cancelled
CI / Build (macos-latest) (pull_request) Has been cancelled
CI / Build (ubuntu-latest) (pull_request) Has been cancelled
CI / Build (windows-latest) (pull_request) Has been cancelled
Some checks failed
ci/woodpecker/pr/check Pipeline failed
CI / Check (pull_request) Has been cancelled
CI / Format (pull_request) Has been cancelled
CI / Clippy (pull_request) Has been cancelled
CI / Build (macos-latest) (pull_request) Has been cancelled
CI / Build (ubuntu-latest) (pull_request) Has been cancelled
CI / Build (windows-latest) (pull_request) Has been cancelled
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
use crate::app::App;
|
||||
use crate::tdlib::TdClientTrait;
|
||||
use crate::tdlib::AuthState;
|
||||
use crate::tdlib::TdClientTrait;
|
||||
use ratatui::{
|
||||
layout::{Alignment, Constraint, Direction, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Chat list panel: search box, chat items, and user online status.
|
||||
|
||||
use crate::app::App;
|
||||
use crate::app::methods::{compose::ComposeMethods, search::SearchMethods};
|
||||
use crate::app::App;
|
||||
use crate::tdlib::TdClientTrait;
|
||||
use crate::tdlib::UserOnlineStatus;
|
||||
use crate::ui::components;
|
||||
@@ -76,7 +76,9 @@ pub fn render<T: TdClientTrait>(f: &mut Frame, area: Rect, app: &mut App<T>) {
|
||||
app.selected_chat_id
|
||||
} else {
|
||||
let filtered = app.get_filtered_chats();
|
||||
app.chat_list_state.selected().and_then(|i| filtered.get(i).map(|c| c.id))
|
||||
app.chat_list_state
|
||||
.selected()
|
||||
.and_then(|i| filtered.get(i).map(|c| c.id))
|
||||
};
|
||||
let (status_text, status_color) = match status_chat_id {
|
||||
Some(chat_id) => format_user_status(app.td_client.get_user_status_by_chat_id(chat_id)),
|
||||
|
||||
@@ -29,12 +29,7 @@ pub fn render_emoji_picker(
|
||||
let x = area.x + (area.width.saturating_sub(modal_width)) / 2;
|
||||
let y = area.y + (area.height.saturating_sub(modal_height)) / 2;
|
||||
|
||||
let modal_area = Rect::new(
|
||||
x,
|
||||
y,
|
||||
modal_width.min(area.width),
|
||||
modal_height.min(area.height),
|
||||
);
|
||||
let modal_area = Rect::new(x, y, modal_width.min(area.width), modal_height.min(area.height));
|
||||
|
||||
// Очищаем область под модалкой
|
||||
f.render_widget(Clear, modal_area);
|
||||
@@ -87,10 +82,7 @@ pub fn render_emoji_picker(
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw("Добавить "),
|
||||
Span::styled(
|
||||
" [Esc] ",
|
||||
Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::styled(" [Esc] ", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)),
|
||||
Span::raw("Отмена"),
|
||||
]));
|
||||
|
||||
|
||||
@@ -34,10 +34,7 @@ pub fn render_input_field(
|
||||
// Символ под курсором (или █ если курсор в конце)
|
||||
if safe_cursor_pos < chars.len() {
|
||||
let cursor_char = chars[safe_cursor_pos].to_string();
|
||||
spans.push(Span::styled(
|
||||
cursor_char,
|
||||
Style::default().fg(Color::Black).bg(color),
|
||||
));
|
||||
spans.push(Span::styled(cursor_char, Style::default().fg(Color::Black).bg(color)));
|
||||
} else {
|
||||
// Курсор в конце - показываем блок
|
||||
spans.push(Span::styled("█", Style::default().fg(color)));
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::formatting;
|
||||
use crate::tdlib::{MessageInfo, PlaybackState, PlaybackStatus};
|
||||
#[cfg(feature = "images")]
|
||||
use crate::tdlib::PhotoDownloadState;
|
||||
use crate::tdlib::{MessageInfo, PlaybackState, PlaybackStatus};
|
||||
use crate::types::MessageId;
|
||||
use crate::utils::{format_date, format_timestamp_with_tz};
|
||||
use ratatui::{
|
||||
@@ -36,10 +36,7 @@ fn wrap_text_with_offsets(text: &str, max_width: usize) -> Vec<WrappedLine> {
|
||||
}
|
||||
|
||||
if all_lines.is_empty() {
|
||||
all_lines.push(WrappedLine {
|
||||
text: String::new(),
|
||||
start_offset: 0,
|
||||
});
|
||||
all_lines.push(WrappedLine { text: String::new(), start_offset: 0 });
|
||||
}
|
||||
|
||||
all_lines
|
||||
@@ -48,10 +45,7 @@ fn wrap_text_with_offsets(text: &str, max_width: usize) -> Vec<WrappedLine> {
|
||||
/// Разбивает один абзац (без `\n`) на строки по ширине
|
||||
fn wrap_paragraph(text: &str, max_width: usize, base_offset: usize) -> Vec<WrappedLine> {
|
||||
if max_width == 0 {
|
||||
return vec![WrappedLine {
|
||||
text: text.to_string(),
|
||||
start_offset: base_offset,
|
||||
}];
|
||||
return vec![WrappedLine { text: text.to_string(), start_offset: base_offset }];
|
||||
}
|
||||
|
||||
let mut result = Vec::new();
|
||||
@@ -122,10 +116,7 @@ fn wrap_paragraph(text: &str, max_width: usize, base_offset: usize) -> Vec<Wrapp
|
||||
}
|
||||
|
||||
if result.is_empty() {
|
||||
result.push(WrappedLine {
|
||||
text: String::new(),
|
||||
start_offset: base_offset,
|
||||
});
|
||||
result.push(WrappedLine { text: String::new(), start_offset: base_offset });
|
||||
}
|
||||
|
||||
result
|
||||
@@ -138,7 +129,11 @@ fn wrap_paragraph(text: &str, max_width: usize, base_offset: usize) -> Vec<Wrapp
|
||||
/// * `date` - timestamp сообщения
|
||||
/// * `content_width` - ширина области для центрирования
|
||||
/// * `is_first` - первый ли это разделитель (если нет, добавляется пустая строка сверху)
|
||||
pub fn render_date_separator(date: i32, content_width: usize, is_first: bool) -> Vec<Line<'static>> {
|
||||
pub fn render_date_separator(
|
||||
date: i32,
|
||||
content_width: usize,
|
||||
is_first: bool,
|
||||
) -> Vec<Line<'static>> {
|
||||
let mut lines = Vec::new();
|
||||
|
||||
if !is_first {
|
||||
@@ -276,10 +271,8 @@ pub fn render_message_bubble(
|
||||
Span::styled(reply_line, Style::default().fg(Color::Cyan)),
|
||||
]));
|
||||
} else {
|
||||
lines.push(Line::from(vec![Span::styled(
|
||||
reply_line,
|
||||
Style::default().fg(Color::Cyan),
|
||||
)]));
|
||||
lines
|
||||
.push(Line::from(vec![Span::styled(reply_line, Style::default().fg(Color::Cyan))]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,9 +294,13 @@ pub fn render_message_bubble(
|
||||
let is_last_line = i == total_wrapped - 1;
|
||||
let line_len = wrapped.text.chars().count();
|
||||
|
||||
let line_entities =
|
||||
formatting::adjust_entities_for_substring(msg.entities(), wrapped.start_offset, line_len);
|
||||
let formatted_spans = formatting::format_text_with_entities(&wrapped.text, &line_entities, msg_color);
|
||||
let line_entities = formatting::adjust_entities_for_substring(
|
||||
msg.entities(),
|
||||
wrapped.start_offset,
|
||||
line_len,
|
||||
);
|
||||
let formatted_spans =
|
||||
formatting::format_text_with_entities(&wrapped.text, &line_entities, msg_color);
|
||||
|
||||
if is_last_line {
|
||||
let full_len = line_len + time_mark_len + marker_len;
|
||||
@@ -313,14 +310,19 @@ pub fn render_message_bubble(
|
||||
// Одна строка — маркер на ней
|
||||
line_spans.push(Span::styled(
|
||||
selection_marker,
|
||||
Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD),
|
||||
Style::default()
|
||||
.fg(Color::Yellow)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
));
|
||||
} else if is_selected {
|
||||
// Последняя строка multi-line — пробелы вместо маркера
|
||||
line_spans.push(Span::raw(" ".repeat(marker_len)));
|
||||
}
|
||||
line_spans.extend(formatted_spans);
|
||||
line_spans.push(Span::styled(format!(" {}", time_mark), Style::default().fg(Color::Gray)));
|
||||
line_spans.push(Span::styled(
|
||||
format!(" {}", time_mark),
|
||||
Style::default().fg(Color::Gray),
|
||||
));
|
||||
lines.push(Line::from(line_spans));
|
||||
} else {
|
||||
let padding = content_width.saturating_sub(line_len + marker_len + 1);
|
||||
@@ -328,7 +330,9 @@ pub fn render_message_bubble(
|
||||
if i == 0 && is_selected {
|
||||
line_spans.push(Span::styled(
|
||||
selection_marker,
|
||||
Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD),
|
||||
Style::default()
|
||||
.fg(Color::Yellow)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
));
|
||||
} else if is_selected {
|
||||
// Средние строки multi-line — пробелы вместо маркера
|
||||
@@ -350,19 +354,26 @@ pub fn render_message_bubble(
|
||||
for (i, wrapped) in wrapped_lines.into_iter().enumerate() {
|
||||
let line_len = wrapped.text.chars().count();
|
||||
|
||||
let line_entities =
|
||||
formatting::adjust_entities_for_substring(msg.entities(), wrapped.start_offset, line_len);
|
||||
let formatted_spans = formatting::format_text_with_entities(&wrapped.text, &line_entities, msg_color);
|
||||
let line_entities = formatting::adjust_entities_for_substring(
|
||||
msg.entities(),
|
||||
wrapped.start_offset,
|
||||
line_len,
|
||||
);
|
||||
let formatted_spans =
|
||||
formatting::format_text_with_entities(&wrapped.text, &line_entities, msg_color);
|
||||
|
||||
if i == 0 {
|
||||
let mut line_spans = vec![];
|
||||
if is_selected {
|
||||
line_spans.push(Span::styled(
|
||||
selection_marker,
|
||||
Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD),
|
||||
Style::default()
|
||||
.fg(Color::Yellow)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
));
|
||||
}
|
||||
line_spans.push(Span::styled(format!(" {}", time_str), Style::default().fg(Color::Gray)));
|
||||
line_spans
|
||||
.push(Span::styled(format!(" {}", time_str), Style::default().fg(Color::Gray)));
|
||||
line_spans.push(Span::raw(" "));
|
||||
line_spans.extend(formatted_spans);
|
||||
lines.push(Line::from(line_spans));
|
||||
@@ -439,10 +450,7 @@ pub fn render_message_bubble(
|
||||
_ => "⏹",
|
||||
};
|
||||
let bar = render_progress_bar(ps.position, ps.duration, 20);
|
||||
format!(
|
||||
"{} {} {:.0}s/{:.0}s",
|
||||
icon, bar, ps.position, ps.duration
|
||||
)
|
||||
format!("{} {} {:.0}s/{:.0}s", icon, bar, ps.position, ps.duration)
|
||||
} else {
|
||||
let waveform = render_waveform(&voice.waveform, 20);
|
||||
format!(" {} {:.0}s", waveform, voice.duration)
|
||||
@@ -456,10 +464,7 @@ pub fn render_message_bubble(
|
||||
Span::styled(status_line, Style::default().fg(Color::Cyan)),
|
||||
]));
|
||||
} else {
|
||||
lines.push(Line::from(Span::styled(
|
||||
status_line,
|
||||
Style::default().fg(Color::Cyan),
|
||||
)));
|
||||
lines.push(Line::from(Span::styled(status_line, Style::default().fg(Color::Cyan))));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -477,10 +482,8 @@ pub fn render_message_bubble(
|
||||
Span::styled(status, Style::default().fg(Color::Yellow)),
|
||||
]));
|
||||
} else {
|
||||
lines.push(Line::from(Span::styled(
|
||||
status,
|
||||
Style::default().fg(Color::Yellow),
|
||||
)));
|
||||
lines
|
||||
.push(Line::from(Span::styled(status, Style::default().fg(Color::Yellow))));
|
||||
}
|
||||
}
|
||||
PhotoDownloadState::Error(e) => {
|
||||
@@ -492,10 +495,7 @@ pub fn render_message_bubble(
|
||||
Span::styled(status, Style::default().fg(Color::Red)),
|
||||
]));
|
||||
} else {
|
||||
lines.push(Line::from(Span::styled(
|
||||
status,
|
||||
Style::default().fg(Color::Red),
|
||||
)));
|
||||
lines.push(Line::from(Span::styled(status, Style::default().fg(Color::Red))));
|
||||
}
|
||||
}
|
||||
PhotoDownloadState::Downloaded(_) => {
|
||||
@@ -540,7 +540,9 @@ pub fn render_album_bubble(
|
||||
content_width: usize,
|
||||
selected_msg_id: Option<MessageId>,
|
||||
) -> (Vec<Line<'static>>, Vec<DeferredImageRender>) {
|
||||
use crate::constants::{ALBUM_GRID_MAX_COLS, ALBUM_PHOTO_GAP, ALBUM_PHOTO_HEIGHT, ALBUM_PHOTO_WIDTH};
|
||||
use crate::constants::{
|
||||
ALBUM_GRID_MAX_COLS, ALBUM_PHOTO_GAP, ALBUM_PHOTO_HEIGHT, ALBUM_PHOTO_WIDTH,
|
||||
};
|
||||
|
||||
let mut lines: Vec<Line<'static>> = Vec::new();
|
||||
let mut deferred: Vec<DeferredImageRender> = Vec::new();
|
||||
@@ -569,12 +571,12 @@ pub fn render_album_bubble(
|
||||
|
||||
// Добавляем маркер выбора на первую строку
|
||||
if is_selected {
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled(
|
||||
selection_marker,
|
||||
Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD),
|
||||
),
|
||||
]));
|
||||
lines.push(Line::from(vec![Span::styled(
|
||||
selection_marker,
|
||||
Style::default()
|
||||
.fg(Color::Yellow)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
)]));
|
||||
}
|
||||
|
||||
let grid_start_line = lines.len();
|
||||
@@ -608,7 +610,9 @@ pub fn render_album_bubble(
|
||||
let x_off = if is_outgoing {
|
||||
let grid_width = cols as u16 * ALBUM_PHOTO_WIDTH
|
||||
+ (cols as u16).saturating_sub(1) * ALBUM_PHOTO_GAP;
|
||||
let padding = content_width.saturating_sub(grid_width as usize + 1) as u16;
|
||||
let padding = content_width
|
||||
.saturating_sub(grid_width as usize + 1)
|
||||
as u16;
|
||||
padding + col as u16 * (ALBUM_PHOTO_WIDTH + ALBUM_PHOTO_GAP)
|
||||
} else {
|
||||
col as u16 * (ALBUM_PHOTO_WIDTH + ALBUM_PHOTO_GAP)
|
||||
@@ -617,7 +621,8 @@ pub fn render_album_bubble(
|
||||
deferred.push(DeferredImageRender {
|
||||
message_id: msg.id(),
|
||||
photo_path: path.clone(),
|
||||
line_offset: grid_start_line + row * ALBUM_PHOTO_HEIGHT as usize,
|
||||
line_offset: grid_start_line
|
||||
+ row * ALBUM_PHOTO_HEIGHT as usize,
|
||||
x_offset: x_off,
|
||||
width: ALBUM_PHOTO_WIDTH,
|
||||
height: ALBUM_PHOTO_HEIGHT,
|
||||
@@ -644,10 +649,7 @@ pub fn render_album_bubble(
|
||||
}
|
||||
PhotoDownloadState::NotDownloaded => {
|
||||
if line_in_row == ALBUM_PHOTO_HEIGHT / 2 {
|
||||
spans.push(Span::styled(
|
||||
"📷",
|
||||
Style::default().fg(Color::Gray),
|
||||
));
|
||||
spans.push(Span::styled("📷", Style::default().fg(Color::Gray)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -706,9 +708,10 @@ pub fn render_album_bubble(
|
||||
Span::styled(time_text, Style::default().fg(Color::Gray)),
|
||||
]));
|
||||
} else {
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled(format!(" {}", time_text), Style::default().fg(Color::Gray)),
|
||||
]));
|
||||
lines.push(Line::from(vec![Span::styled(
|
||||
format!(" {}", time_text),
|
||||
Style::default().fg(Color::Gray),
|
||||
)]));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -91,7 +91,10 @@ pub fn calculate_scroll_offset(
|
||||
}
|
||||
|
||||
/// Renders a help bar with keyboard shortcuts
|
||||
pub fn render_help_bar(shortcuts: &[(&str, &str, Color)], border_color: Color) -> Paragraph<'static> {
|
||||
pub fn render_help_bar(
|
||||
shortcuts: &[(&str, &str, Color)],
|
||||
border_color: Color,
|
||||
) -> Paragraph<'static> {
|
||||
let mut spans: Vec<Span<'static>> = Vec::new();
|
||||
for (i, (key, label, color)) in shortcuts.iter().enumerate() {
|
||||
if i > 0 {
|
||||
@@ -99,9 +102,7 @@ pub fn render_help_bar(shortcuts: &[(&str, &str, Color)], border_color: Color) -
|
||||
}
|
||||
spans.push(Span::styled(
|
||||
format!(" {} ", key),
|
||||
Style::default()
|
||||
.fg(*color)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
Style::default().fg(*color).add_modifier(Modifier::BOLD),
|
||||
));
|
||||
spans.push(Span::raw(label.to_string()));
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
//! Reusable UI components: message bubbles, input fields, modals, lists.
|
||||
|
||||
pub mod modal;
|
||||
pub mod chat_list_item;
|
||||
pub mod emoji_picker;
|
||||
pub mod input_field;
|
||||
pub mod message_bubble;
|
||||
pub mod message_list;
|
||||
pub mod chat_list_item;
|
||||
pub mod emoji_picker;
|
||||
pub mod modal;
|
||||
|
||||
// Экспорт основных функций
|
||||
pub use input_field::render_input_field;
|
||||
pub use chat_list_item::render_chat_list_item;
|
||||
pub use emoji_picker::render_emoji_picker;
|
||||
pub use message_bubble::{render_date_separator, render_message_bubble, render_sender_header};
|
||||
pub use input_field::render_input_field;
|
||||
#[cfg(feature = "images")]
|
||||
pub use message_bubble::{DeferredImageRender, calculate_image_height, render_album_bubble};
|
||||
pub use message_list::{render_message_item, calculate_scroll_offset, render_help_bar};
|
||||
pub use message_bubble::{calculate_image_height, render_album_bubble, DeferredImageRender};
|
||||
pub use message_bubble::{render_date_separator, render_message_bubble, render_sender_header};
|
||||
pub use message_list::{calculate_scroll_offset, render_help_bar, render_message_item};
|
||||
|
||||
@@ -74,10 +74,7 @@ pub fn render_delete_confirm_modal(f: &mut Frame, area: Rect) {
|
||||
),
|
||||
Span::raw("Да"),
|
||||
Span::raw(" "),
|
||||
Span::styled(
|
||||
" [n/Esc] ",
|
||||
Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::styled(" [n/Esc] ", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)),
|
||||
Span::raw("Нет"),
|
||||
]),
|
||||
];
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
//! Compose bar / input box rendering
|
||||
|
||||
use crate::app::methods::{compose::ComposeMethods, messages::MessageMethods};
|
||||
use crate::app::App;
|
||||
use crate::app::InputMode;
|
||||
use crate::app::methods::{compose::ComposeMethods, messages::MessageMethods};
|
||||
use crate::tdlib::TdClientTrait;
|
||||
use crate::ui::components;
|
||||
use ratatui::{
|
||||
@@ -124,13 +124,18 @@ pub fn render<T: TdClientTrait>(f: &mut Frame, area: Rect, app: &App<T>) {
|
||||
} else if app.input_mode == InputMode::Normal {
|
||||
// Normal mode — dim, no cursor
|
||||
if app.message_input.is_empty() {
|
||||
let line = Line::from(vec![
|
||||
Span::styled("> Press i to type...", Style::default().fg(Color::DarkGray)),
|
||||
]);
|
||||
let line = Line::from(vec![Span::styled(
|
||||
"> Press i to type...",
|
||||
Style::default().fg(Color::DarkGray),
|
||||
)]);
|
||||
(line, "")
|
||||
} else {
|
||||
let draft_preview: String = app.message_input.chars().take(60).collect();
|
||||
let ellipsis = if app.message_input.chars().count() > 60 { "..." } else { "" };
|
||||
let ellipsis = if app.message_input.chars().count() > 60 {
|
||||
"..."
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let line = Line::from(Span::styled(
|
||||
format!("> {}{}", draft_preview, ellipsis),
|
||||
Style::default().fg(Color::DarkGray),
|
||||
@@ -163,7 +168,9 @@ pub fn render<T: TdClientTrait>(f: &mut Frame, area: Rect, app: &App<T>) {
|
||||
} else {
|
||||
Style::default().fg(Color::DarkGray)
|
||||
};
|
||||
Block::default().borders(Borders::ALL).border_style(border_style)
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(border_style)
|
||||
} else {
|
||||
let title_color = if app.is_replying() || app.is_forwarding() {
|
||||
Color::Cyan
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::app::App;
|
||||
use crate::app::InputMode;
|
||||
use crate::tdlib::TdClientTrait;
|
||||
use crate::tdlib::NetworkState;
|
||||
use crate::tdlib::TdClientTrait;
|
||||
use ratatui::{
|
||||
layout::Rect,
|
||||
style::{Color, Style},
|
||||
@@ -31,7 +31,10 @@ pub fn render<T: TdClientTrait>(f: &mut Frame, area: Rect, app: &App<T>) {
|
||||
} else if let Some(err) = &app.error_message {
|
||||
format!(" {}{}Error: {} ", account_indicator, network_indicator, err)
|
||||
} else if app.is_searching {
|
||||
format!(" {}{}↑/↓: Navigate | Enter: Select | Esc: Cancel ", account_indicator, network_indicator)
|
||||
format!(
|
||||
" {}{}↑/↓: Navigate | Enter: Select | Esc: Cancel ",
|
||||
account_indicator, network_indicator
|
||||
)
|
||||
} else if app.selected_chat_id.is_some() {
|
||||
let mode_str = match app.input_mode {
|
||||
InputMode::Normal => "[NORMAL] j/k: Nav | i: Insert | d/r/f/y: Actions | Esc: Close",
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
//! Renders message bubbles grouped by date/sender, pinned bar, and delegates
|
||||
//! to modals (search, pinned, reactions, delete) and compose_bar.
|
||||
|
||||
use crate::app::App;
|
||||
use crate::app::methods::{messages::MessageMethods, modal::ModalMethods, search::SearchMethods};
|
||||
use crate::tdlib::TdClientTrait;
|
||||
use crate::app::App;
|
||||
use crate::message_grouping::{group_messages, MessageGroup};
|
||||
use crate::tdlib::TdClientTrait;
|
||||
use crate::ui::components;
|
||||
use crate::ui::{compose_bar, modals};
|
||||
use ratatui::{
|
||||
@@ -18,7 +18,12 @@ use ratatui::{
|
||||
};
|
||||
|
||||
/// Рендерит заголовок чата с typing status
|
||||
fn render_chat_header<T: TdClientTrait>(f: &mut Frame, area: Rect, app: &App<T>, chat: &crate::tdlib::ChatInfo) {
|
||||
fn render_chat_header<T: TdClientTrait>(
|
||||
f: &mut Frame,
|
||||
area: Rect,
|
||||
app: &App<T>,
|
||||
chat: &crate::tdlib::ChatInfo,
|
||||
) {
|
||||
let typing_action = app
|
||||
.td_client
|
||||
.typing_status()
|
||||
@@ -34,10 +39,7 @@ fn render_chat_header<T: TdClientTrait>(f: &mut Frame, area: Rect, app: &App<T>,
|
||||
.add_modifier(Modifier::BOLD),
|
||||
)];
|
||||
if let Some(username) = &chat.username {
|
||||
spans.push(Span::styled(
|
||||
format!(" {}", username),
|
||||
Style::default().fg(Color::Gray),
|
||||
));
|
||||
spans.push(Span::styled(format!(" {}", username), Style::default().fg(Color::Gray)));
|
||||
}
|
||||
spans.push(Span::styled(
|
||||
format!(" {}", action),
|
||||
@@ -90,8 +92,7 @@ fn render_pinned_bar<T: TdClientTrait>(f: &mut Frame, area: Rect, app: &App<T>)
|
||||
Span::raw(" ".repeat(padding)),
|
||||
Span::styled(pinned_hint, Style::default().fg(Color::Gray)),
|
||||
]);
|
||||
let pinned_bar =
|
||||
Paragraph::new(pinned_line).style(Style::default().bg(Color::Rgb(40, 20, 40)));
|
||||
let pinned_bar = Paragraph::new(pinned_line).style(Style::default().bg(Color::Rgb(40, 20, 40)));
|
||||
f.render_widget(pinned_bar, area);
|
||||
}
|
||||
|
||||
@@ -104,9 +105,7 @@ pub(super) struct WrappedLine {
|
||||
/// (используется только для search/pinned режимов, основной рендеринг через message_bubble)
|
||||
pub(super) fn wrap_text_with_offsets(text: &str, max_width: usize) -> Vec<WrappedLine> {
|
||||
if max_width == 0 {
|
||||
return vec![WrappedLine {
|
||||
text: text.to_string(),
|
||||
}];
|
||||
return vec![WrappedLine { text: text.to_string() }];
|
||||
}
|
||||
|
||||
let mut result = Vec::new();
|
||||
@@ -131,9 +130,7 @@ pub(super) fn wrap_text_with_offsets(text: &str, max_width: usize) -> Vec<Wrappe
|
||||
current_line.push_str(&word);
|
||||
current_width += 1 + word_width;
|
||||
} else {
|
||||
result.push(WrappedLine {
|
||||
text: current_line,
|
||||
});
|
||||
result.push(WrappedLine { text: current_line });
|
||||
current_line = word;
|
||||
current_width = word_width;
|
||||
}
|
||||
@@ -155,23 +152,17 @@ pub(super) fn wrap_text_with_offsets(text: &str, max_width: usize) -> Vec<Wrappe
|
||||
current_line.push(' ');
|
||||
current_line.push_str(&word);
|
||||
} else {
|
||||
result.push(WrappedLine {
|
||||
text: current_line,
|
||||
});
|
||||
result.push(WrappedLine { text: current_line });
|
||||
current_line = word;
|
||||
}
|
||||
}
|
||||
|
||||
if !current_line.is_empty() {
|
||||
result.push(WrappedLine {
|
||||
text: current_line,
|
||||
});
|
||||
result.push(WrappedLine { text: current_line });
|
||||
}
|
||||
|
||||
if result.is_empty() {
|
||||
result.push(WrappedLine {
|
||||
text: String::new(),
|
||||
});
|
||||
result.push(WrappedLine { text: String::new() });
|
||||
}
|
||||
|
||||
result
|
||||
@@ -208,10 +199,7 @@ fn render_message_list<T: TdClientTrait>(f: &mut Frame, area: Rect, app: &mut Ap
|
||||
is_first_date = false;
|
||||
is_first_sender = true; // Сбрасываем счётчик заголовков после даты
|
||||
}
|
||||
MessageGroup::SenderHeader {
|
||||
is_outgoing,
|
||||
sender_name,
|
||||
} => {
|
||||
MessageGroup::SenderHeader { is_outgoing, sender_name } => {
|
||||
// Рендерим заголовок отправителя
|
||||
lines.extend(components::render_sender_header(
|
||||
is_outgoing,
|
||||
@@ -240,9 +228,16 @@ fn render_message_list<T: TdClientTrait>(f: &mut Frame, area: Rect, app: &mut Ap
|
||||
// Собираем deferred image renders для всех загруженных фото
|
||||
#[cfg(feature = "images")]
|
||||
if let Some(photo) = msg.photo_info() {
|
||||
if let crate::tdlib::PhotoDownloadState::Downloaded(path) = &photo.download_state {
|
||||
let inline_width = content_width.min(crate::constants::INLINE_IMAGE_MAX_WIDTH);
|
||||
let img_height = components::calculate_image_height(photo.width, photo.height, inline_width);
|
||||
if let crate::tdlib::PhotoDownloadState::Downloaded(path) =
|
||||
&photo.download_state
|
||||
{
|
||||
let inline_width =
|
||||
content_width.min(crate::constants::INLINE_IMAGE_MAX_WIDTH);
|
||||
let img_height = components::calculate_image_height(
|
||||
photo.width,
|
||||
photo.height,
|
||||
inline_width,
|
||||
);
|
||||
let img_width = inline_width as u16;
|
||||
let bubble_len = bubble_lines.len();
|
||||
let placeholder_start = lines.len() + bubble_len - img_height as usize;
|
||||
@@ -352,7 +347,8 @@ fn render_message_list<T: TdClientTrait>(f: &mut Frame, area: Rect, app: &mut Ap
|
||||
use ratatui_image::StatefulImage;
|
||||
|
||||
// THROTTLING: Рендерим изображения максимум 15 FPS (каждые 66ms)
|
||||
let should_render_images = app.last_image_render_time
|
||||
let should_render_images = app
|
||||
.last_image_render_time
|
||||
.map(|t| t.elapsed() > std::time::Duration::from_millis(66))
|
||||
.unwrap_or(true);
|
||||
|
||||
@@ -384,7 +380,7 @@ fn render_message_list<T: TdClientTrait>(f: &mut Frame, area: Rect, app: &mut Ap
|
||||
if let Some(renderer) = &mut app.inline_image_renderer {
|
||||
// Загружаем только если видимо (early return если уже в кеше)
|
||||
let _ = renderer.load_image(d.message_id, &d.photo_path);
|
||||
|
||||
|
||||
if let Some(protocol) = renderer.get_protocol(&d.message_id) {
|
||||
f.render_stateful_widget(StatefulImage::default(), img_rect, protocol);
|
||||
}
|
||||
@@ -487,14 +483,9 @@ pub fn render<T: TdClientTrait>(f: &mut Frame, area: Rect, app: &mut App<T>) {
|
||||
}
|
||||
|
||||
// Модалка выбора реакции
|
||||
if let crate::app::ChatState::ReactionPicker {
|
||||
available_reactions,
|
||||
selected_index,
|
||||
..
|
||||
} = &app.chat_state
|
||||
if let crate::app::ChatState::ReactionPicker { available_reactions, selected_index, .. } =
|
||||
&app.chat_state
|
||||
{
|
||||
modals::render_reaction_picker(f, area, available_reactions, *selected_index);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
|
||||
mod auth;
|
||||
pub mod chat_list;
|
||||
mod compose_bar;
|
||||
pub mod components;
|
||||
mod compose_bar;
|
||||
pub mod footer;
|
||||
mod loading;
|
||||
mod main_screen;
|
||||
|
||||
@@ -20,18 +20,10 @@ pub fn render<T: TdClientTrait>(f: &mut Frame, area: Rect, app: &App<T>) {
|
||||
};
|
||||
|
||||
match state {
|
||||
AccountSwitcherState::SelectAccount {
|
||||
accounts,
|
||||
selected_index,
|
||||
current_account,
|
||||
} => {
|
||||
AccountSwitcherState::SelectAccount { accounts, selected_index, current_account } => {
|
||||
render_select_account(f, area, accounts, *selected_index, current_account);
|
||||
}
|
||||
AccountSwitcherState::AddAccount {
|
||||
name_input,
|
||||
cursor_position,
|
||||
error,
|
||||
} => {
|
||||
AccountSwitcherState::AddAccount { name_input, cursor_position, error } => {
|
||||
render_add_account(f, area, name_input, *cursor_position, error.as_deref());
|
||||
}
|
||||
}
|
||||
@@ -53,10 +45,7 @@ fn render_select_account(
|
||||
|
||||
let marker = if is_current { "● " } else { " " };
|
||||
let suffix = if is_current { " (текущий)" } else { "" };
|
||||
let display = format!(
|
||||
"{}{} ({}){}",
|
||||
marker, account.name, account.display_name, suffix
|
||||
);
|
||||
let display = format!("{}{} ({}){}", marker, account.name, account.display_name, suffix);
|
||||
|
||||
let style = if is_selected {
|
||||
Style::default()
|
||||
@@ -86,10 +75,7 @@ fn render_select_account(
|
||||
} else {
|
||||
Style::default().fg(Color::Cyan)
|
||||
};
|
||||
lines.push(Line::from(Span::styled(
|
||||
" + Добавить аккаунт",
|
||||
add_style,
|
||||
)));
|
||||
lines.push(Line::from(Span::styled(" + Добавить аккаунт", add_style)));
|
||||
|
||||
lines.push(Line::from(""));
|
||||
|
||||
@@ -148,10 +134,7 @@ fn render_add_account(
|
||||
let input_display = if name_input.is_empty() {
|
||||
Span::styled("_", Style::default().fg(Color::DarkGray))
|
||||
} else {
|
||||
Span::styled(
|
||||
format!("{}_", name_input),
|
||||
Style::default().fg(Color::White),
|
||||
)
|
||||
Span::styled(format!("{}_", name_input), Style::default().fg(Color::White))
|
||||
};
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled(" Имя: ", Style::default().fg(Color::Cyan)),
|
||||
@@ -168,10 +151,7 @@ fn render_add_account(
|
||||
|
||||
// Error
|
||||
if let Some(err) = error {
|
||||
lines.push(Line::from(Span::styled(
|
||||
format!(" {}", err),
|
||||
Style::default().fg(Color::Red),
|
||||
)));
|
||||
lines.push(Line::from(Span::styled(format!(" {}", err), Style::default().fg(Color::Red))));
|
||||
lines.push(Line::from(""));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Delete confirmation modal
|
||||
|
||||
use ratatui::{Frame, layout::Rect};
|
||||
use ratatui::{layout::Rect, Frame};
|
||||
|
||||
/// Renders delete confirmation modal
|
||||
pub fn render(f: &mut Frame, area: Rect) {
|
||||
|
||||
@@ -19,19 +19,12 @@ use ratatui::{
|
||||
use ratatui_image::StatefulImage;
|
||||
|
||||
/// Рендерит модальное окно с полноэкранным изображением
|
||||
pub fn render<T: TdClientTrait>(
|
||||
f: &mut Frame,
|
||||
app: &mut App<T>,
|
||||
modal_state: &ImageModalState,
|
||||
) {
|
||||
pub fn render<T: TdClientTrait>(f: &mut Frame, app: &mut App<T>, modal_state: &ImageModalState) {
|
||||
let area = f.area();
|
||||
|
||||
// Затемняем весь фон
|
||||
f.render_widget(Clear, area);
|
||||
f.render_widget(
|
||||
Block::default().style(Style::default().bg(Color::Black)),
|
||||
area,
|
||||
);
|
||||
f.render_widget(Block::default().style(Style::default().bg(Color::Black)), area);
|
||||
|
||||
// Резервируем место для подсказок (2 строки внизу)
|
||||
let image_area_height = area.height.saturating_sub(2);
|
||||
@@ -76,7 +69,7 @@ pub fn render<T: TdClientTrait>(
|
||||
|
||||
// Загружаем изображение (может занять время для iTerm2/Sixel)
|
||||
let _ = renderer.load_image(modal_state.message_id, &modal_state.photo_path);
|
||||
|
||||
|
||||
// Триггерим перерисовку для показа загруженного изображения
|
||||
app.needs_redraw = true;
|
||||
}
|
||||
|
||||
@@ -10,18 +10,18 @@
|
||||
|
||||
pub mod account_switcher;
|
||||
pub mod delete_confirm;
|
||||
pub mod pinned;
|
||||
pub mod reaction_picker;
|
||||
pub mod search;
|
||||
pub mod pinned;
|
||||
|
||||
#[cfg(feature = "images")]
|
||||
pub mod image_viewer;
|
||||
|
||||
pub use account_switcher::render as render_account_switcher;
|
||||
pub use delete_confirm::render as render_delete_confirm;
|
||||
pub use pinned::render as render_pinned;
|
||||
pub use reaction_picker::render as render_reaction_picker;
|
||||
pub use search::render as render_search;
|
||||
pub use pinned::render as render_pinned;
|
||||
|
||||
#[cfg(feature = "images")]
|
||||
pub use image_viewer::render as render_image_viewer;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
use crate::app::App;
|
||||
use crate::tdlib::TdClientTrait;
|
||||
use crate::ui::components::{render_message_item, calculate_scroll_offset, render_help_bar};
|
||||
use crate::ui::components::{calculate_scroll_offset, render_help_bar, render_message_item};
|
||||
use ratatui::{
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
@@ -14,15 +14,13 @@ use ratatui::{
|
||||
/// Renders pinned messages mode
|
||||
pub fn render<T: TdClientTrait>(f: &mut Frame, area: Rect, app: &App<T>) {
|
||||
// Извлекаем данные из ChatState
|
||||
let (messages, selected_index) = if let crate::app::ChatState::PinnedMessages {
|
||||
messages,
|
||||
selected_index,
|
||||
} = &app.chat_state
|
||||
{
|
||||
(messages.as_slice(), *selected_index)
|
||||
} else {
|
||||
return; // Некорректное состояние
|
||||
};
|
||||
let (messages, selected_index) =
|
||||
if let crate::app::ChatState::PinnedMessages { messages, selected_index } = &app.chat_state
|
||||
{
|
||||
(messages.as_slice(), *selected_index)
|
||||
} else {
|
||||
return; // Некорректное состояние
|
||||
};
|
||||
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
//! Reaction picker modal
|
||||
|
||||
use ratatui::{Frame, layout::Rect};
|
||||
use ratatui::{layout::Rect, Frame};
|
||||
|
||||
/// Renders emoji reaction picker modal
|
||||
pub fn render(
|
||||
f: &mut Frame,
|
||||
area: Rect,
|
||||
available_reactions: &[String],
|
||||
selected_index: usize,
|
||||
) {
|
||||
pub fn render(f: &mut Frame, area: Rect, available_reactions: &[String], selected_index: usize) {
|
||||
crate::ui::components::render_emoji_picker(f, area, available_reactions, selected_index);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
use crate::app::App;
|
||||
use crate::tdlib::TdClientTrait;
|
||||
use crate::ui::components::{render_message_item, calculate_scroll_offset, render_help_bar};
|
||||
use crate::ui::components::{calculate_scroll_offset, render_help_bar, render_message_item};
|
||||
use ratatui::{
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
@@ -15,11 +15,8 @@ use ratatui::{
|
||||
pub fn render<T: TdClientTrait>(f: &mut Frame, area: Rect, app: &App<T>) {
|
||||
// Извлекаем данные из ChatState
|
||||
let (query, results, selected_index) =
|
||||
if let crate::app::ChatState::SearchInChat {
|
||||
query,
|
||||
results,
|
||||
selected_index,
|
||||
} = &app.chat_state
|
||||
if let crate::app::ChatState::SearchInChat { query, results, selected_index } =
|
||||
&app.chat_state
|
||||
{
|
||||
(query.as_str(), results.as_slice(), *selected_index)
|
||||
} else {
|
||||
@@ -37,11 +34,7 @@ pub fn render<T: TdClientTrait>(f: &mut Frame, area: Rect, app: &App<T>) {
|
||||
|
||||
// Search input
|
||||
let total = results.len();
|
||||
let current = if total > 0 {
|
||||
selected_index + 1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let current = if total > 0 { selected_index + 1 } else { 0 };
|
||||
|
||||
let input_line = if query.is_empty() {
|
||||
Line::from(vec![
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::app::App;
|
||||
use crate::app::methods::modal::ModalMethods;
|
||||
use crate::tdlib::TdClientTrait;
|
||||
use crate::app::App;
|
||||
use crate::tdlib::ProfileInfo;
|
||||
use crate::tdlib::TdClientTrait;
|
||||
use ratatui::{
|
||||
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
|
||||
Reference in New Issue
Block a user