commit
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
use crate::app::App;
|
||||
use crate::tdlib::client::AuthState;
|
||||
use ratatui::{
|
||||
layout::{Alignment, Constraint, Direction, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
@@ -5,8 +7,6 @@ use ratatui::{
|
||||
widgets::{Block, Borders, Paragraph},
|
||||
Frame,
|
||||
};
|
||||
use crate::app::App;
|
||||
use crate::tdlib::client::AuthState;
|
||||
|
||||
pub fn render(f: &mut Frame, app: &App) {
|
||||
let area = f.area();
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use crate::app::App;
|
||||
use crate::tdlib::UserOnlineStatus;
|
||||
use ratatui::{
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
widgets::{Block, Borders, List, ListItem, Paragraph},
|
||||
Frame,
|
||||
};
|
||||
use crate::app::App;
|
||||
use crate::tdlib::UserOnlineStatus;
|
||||
|
||||
pub fn render(f: &mut Frame, area: Rect, app: &mut App) {
|
||||
let chat_chunks = Layout::default()
|
||||
@@ -54,7 +54,9 @@ pub fn render(f: &mut Frame, area: Rect, app: &mut App) {
|
||||
|
||||
let prefix = if is_selected { "▌" } else { " " };
|
||||
|
||||
let username_text = chat.username.as_ref()
|
||||
let username_text = chat
|
||||
.username
|
||||
.as_ref()
|
||||
.map(|u| format!(" {}", u))
|
||||
.unwrap_or_default();
|
||||
|
||||
@@ -78,7 +80,18 @@ pub fn render(f: &mut Frame, area: Rect, app: &mut App) {
|
||||
String::new()
|
||||
};
|
||||
|
||||
let content = format!("{}{}{}{}{}{}{}{}{}", prefix, status_icon, pin_icon, mute_icon, chat.title, username_text, mention_badge, draft_badge, unread_badge);
|
||||
let content = format!(
|
||||
"{}{}{}{}{}{}{}{}{}",
|
||||
prefix,
|
||||
status_icon,
|
||||
pin_icon,
|
||||
mute_icon,
|
||||
chat.title,
|
||||
username_text,
|
||||
mention_badge,
|
||||
draft_badge,
|
||||
unread_badge
|
||||
);
|
||||
|
||||
// Цвет: онлайн — зелёные, остальные — белые
|
||||
let style = match app.td_client.get_user_status_by_chat_id(chat.id) {
|
||||
@@ -100,13 +113,11 @@ pub fn render(f: &mut Frame, area: Rect, app: &mut App) {
|
||||
Block::default().borders(Borders::ALL)
|
||||
};
|
||||
|
||||
let chats_list = List::new(items)
|
||||
.block(block)
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.add_modifier(Modifier::ITALIC)
|
||||
.fg(Color::Yellow),
|
||||
);
|
||||
let chats_list = List::new(items).block(block).highlight_style(
|
||||
Style::default()
|
||||
.add_modifier(Modifier::ITALIC)
|
||||
.fg(Color::Yellow),
|
||||
);
|
||||
|
||||
f.render_stateful_widget(chats_list, chat_chunks[1], &mut app.chat_list_state);
|
||||
|
||||
@@ -119,8 +130,12 @@ pub fn render(f: &mut Frame, area: Rect, app: &mut App) {
|
||||
let formatted = format_was_online(*was_online);
|
||||
(formatted, Color::Gray)
|
||||
}
|
||||
Some(UserOnlineStatus::LastWeek) => ("был(а) на этой неделе".to_string(), Color::DarkGray),
|
||||
Some(UserOnlineStatus::LastMonth) => ("был(а) в этом месяце".to_string(), Color::DarkGray),
|
||||
Some(UserOnlineStatus::LastWeek) => {
|
||||
("был(а) на этой неделе".to_string(), Color::DarkGray)
|
||||
}
|
||||
Some(UserOnlineStatus::LastMonth) => {
|
||||
("был(а) в этом месяце".to_string(), Color::DarkGray)
|
||||
}
|
||||
Some(UserOnlineStatus::LongTimeAgo) => ("был(а) давно".to_string(), Color::DarkGray),
|
||||
None => ("".to_string(), Color::DarkGray), // Для групп/каналов
|
||||
}
|
||||
@@ -131,14 +146,22 @@ pub fn render(f: &mut Frame, area: Rect, app: &mut App) {
|
||||
if let Some(chat) = filtered.get(i) {
|
||||
match app.td_client.get_user_status_by_chat_id(chat.id) {
|
||||
Some(UserOnlineStatus::Online) => ("● онлайн".to_string(), Color::Green),
|
||||
Some(UserOnlineStatus::Recently) => ("был(а) недавно".to_string(), Color::Yellow),
|
||||
Some(UserOnlineStatus::Recently) => {
|
||||
("был(а) недавно".to_string(), Color::Yellow)
|
||||
}
|
||||
Some(UserOnlineStatus::Offline(was_online)) => {
|
||||
let formatted = format_was_online(*was_online);
|
||||
(formatted, Color::Gray)
|
||||
}
|
||||
Some(UserOnlineStatus::LastWeek) => ("был(а) на этой неделе".to_string(), Color::DarkGray),
|
||||
Some(UserOnlineStatus::LastMonth) => ("был(а) в этом месяце".to_string(), Color::DarkGray),
|
||||
Some(UserOnlineStatus::LongTimeAgo) => ("был(а) давно".to_string(), Color::DarkGray),
|
||||
Some(UserOnlineStatus::LastWeek) => {
|
||||
("был(а) на этой неделе".to_string(), Color::DarkGray)
|
||||
}
|
||||
Some(UserOnlineStatus::LastMonth) => {
|
||||
("был(а) в этом месяце".to_string(), Color::DarkGray)
|
||||
}
|
||||
Some(UserOnlineStatus::LongTimeAgo) => {
|
||||
("был(а) давно".to_string(), Color::DarkGray)
|
||||
}
|
||||
None => ("".to_string(), Color::DarkGray),
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use crate::app::App;
|
||||
use crate::tdlib::NetworkState;
|
||||
use ratatui::{
|
||||
layout::Rect,
|
||||
style::{Color, Style},
|
||||
widgets::Paragraph,
|
||||
Frame,
|
||||
};
|
||||
use crate::app::App;
|
||||
use crate::tdlib::NetworkState;
|
||||
|
||||
pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
||||
// Индикатор состояния сети
|
||||
@@ -26,7 +26,10 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
||||
} else if app.selected_chat_id.is_some() {
|
||||
format!(" {}↑/↓: Scroll | Ctrl+U: Profile | Enter: Send | Esc: Close | Ctrl+R: Refresh | Ctrl+C: Quit ", network_indicator)
|
||||
} else {
|
||||
format!(" {}↑/↓: Navigate | Enter: Open | Ctrl+S: Search | Ctrl+R: Refresh | Ctrl+C: Quit ", network_indicator)
|
||||
format!(
|
||||
" {}↑/↓: Navigate | Enter: Open | Ctrl+S: Search | Ctrl+R: Refresh | Ctrl+C: Quit ",
|
||||
network_indicator
|
||||
)
|
||||
};
|
||||
|
||||
let style = if matches!(app.td_client.network_state, NetworkState::WaitingForNetwork) {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use crate::app::App;
|
||||
use ratatui::{
|
||||
layout::{Alignment, Constraint, Direction, Layout},
|
||||
style::{Color, Modifier, Style},
|
||||
widgets::{Block, Borders, Paragraph},
|
||||
Frame,
|
||||
};
|
||||
use crate::app::App;
|
||||
|
||||
pub fn render(f: &mut Frame, app: &App) {
|
||||
let area = f.area();
|
||||
@@ -18,10 +18,7 @@ pub fn render(f: &mut Frame, app: &App) {
|
||||
])
|
||||
.split(area);
|
||||
|
||||
let message = app
|
||||
.status_message
|
||||
.as_deref()
|
||||
.unwrap_or("Загрузка...");
|
||||
let message = app.status_message.as_deref().unwrap_or("Загрузка...");
|
||||
|
||||
let loading = Paragraph::new(message)
|
||||
.style(
|
||||
@@ -30,11 +27,7 @@ pub fn render(f: &mut Frame, app: &App) {
|
||||
.add_modifier(Modifier::BOLD),
|
||||
)
|
||||
.alignment(Alignment::Center)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title(" TTUI "),
|
||||
);
|
||||
.block(Block::default().borders(Borders::ALL).title(" TTUI "));
|
||||
|
||||
f.render_widget(loading, chunks[1]);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use super::{chat_list, footer, messages};
|
||||
use crate::app::App;
|
||||
use ratatui::{
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
@@ -5,8 +7,6 @@ use ratatui::{
|
||||
widgets::{Block, Borders, Paragraph},
|
||||
Frame,
|
||||
};
|
||||
use crate::app::App;
|
||||
use super::{chat_list, messages, footer};
|
||||
|
||||
/// Порог ширины для компактного режима (одна панель)
|
||||
const COMPACT_WIDTH: u16 = 80;
|
||||
@@ -81,11 +81,8 @@ fn render_folders(f: &mut Frame, area: Rect, app: &App) {
|
||||
}
|
||||
|
||||
let folders_line = Line::from(spans);
|
||||
let folders_widget = Paragraph::new(folders_line).block(
|
||||
Block::default()
|
||||
.title(" TTUI ")
|
||||
.borders(Borders::ALL),
|
||||
);
|
||||
let folders_widget =
|
||||
Paragraph::new(folders_line).block(Block::default().title(" TTUI ").borders(Borders::ALL));
|
||||
|
||||
f.render_widget(folders_widget, area);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use crate::app::App;
|
||||
use crate::utils::{format_date, format_timestamp_with_tz, get_day};
|
||||
use ratatui::{
|
||||
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
@@ -5,8 +7,6 @@ use ratatui::{
|
||||
widgets::{Block, Borders, Paragraph},
|
||||
Frame,
|
||||
};
|
||||
use crate::app::App;
|
||||
use crate::utils::{format_timestamp_with_tz, format_date, get_day};
|
||||
use tdlib_rs::enums::TextEntityType;
|
||||
use tdlib_rs::types::TextEntity;
|
||||
|
||||
@@ -58,9 +58,16 @@ impl CharStyle {
|
||||
}
|
||||
|
||||
/// Преобразует текст с entities в вектор стилизованных Span (owned)
|
||||
fn format_text_with_entities(text: &str, entities: &[TextEntity], base_color: Color) -> Vec<Span<'static>> {
|
||||
fn format_text_with_entities(
|
||||
text: &str,
|
||||
entities: &[TextEntity],
|
||||
base_color: Color,
|
||||
) -> Vec<Span<'static>> {
|
||||
if entities.is_empty() {
|
||||
return vec![Span::styled(text.to_string(), Style::default().fg(base_color))];
|
||||
return vec![Span::styled(
|
||||
text.to_string(),
|
||||
Style::default().fg(base_color),
|
||||
)];
|
||||
}
|
||||
|
||||
// Создаём массив стилей для каждого символа
|
||||
@@ -82,9 +89,13 @@ fn format_text_with_entities(text: &str, entities: &[TextEntity], base_color: Co
|
||||
char_styles[i].code = true
|
||||
}
|
||||
TextEntityType::Spoiler => char_styles[i].spoiler = true,
|
||||
TextEntityType::Url | TextEntityType::TextUrl(_) | TextEntityType::EmailAddress
|
||||
TextEntityType::Url
|
||||
| TextEntityType::TextUrl(_)
|
||||
| TextEntityType::EmailAddress
|
||||
| TextEntityType::PhoneNumber => char_styles[i].url = true,
|
||||
TextEntityType::Mention | TextEntityType::MentionName(_) => char_styles[i].mention = true,
|
||||
TextEntityType::Mention | TextEntityType::MentionName(_) => {
|
||||
char_styles[i].mention = true
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -144,7 +155,12 @@ fn styles_equal(a: &CharStyle, b: &CharStyle) -> bool {
|
||||
}
|
||||
|
||||
/// Рендерит текст инпута с блочным курсором
|
||||
fn render_input_with_cursor(prefix: &str, text: &str, cursor_pos: usize, color: Color) -> Line<'static> {
|
||||
fn render_input_with_cursor(
|
||||
prefix: &str,
|
||||
text: &str,
|
||||
cursor_pos: usize,
|
||||
color: Color,
|
||||
) -> Line<'static> {
|
||||
let chars: Vec<char> = text.chars().collect();
|
||||
let mut spans: Vec<Span> = vec![Span::raw(prefix.to_string())];
|
||||
|
||||
@@ -186,10 +202,7 @@ struct WrappedLine {
|
||||
/// Возвращает строки с информацией о позициях для корректного применения entities
|
||||
fn wrap_text_with_offsets(text: &str, max_width: usize) -> Vec<WrappedLine> {
|
||||
if max_width == 0 {
|
||||
return vec![WrappedLine {
|
||||
text: text.to_string(),
|
||||
start_offset: 0,
|
||||
}];
|
||||
return vec![WrappedLine { text: text.to_string(), start_offset: 0 }];
|
||||
}
|
||||
|
||||
let mut result = Vec::new();
|
||||
@@ -263,10 +276,7 @@ fn wrap_text_with_offsets(text: &str, max_width: usize) -> Vec<WrappedLine> {
|
||||
}
|
||||
|
||||
if result.is_empty() {
|
||||
result.push(WrappedLine {
|
||||
text: String::new(),
|
||||
start_offset: 0,
|
||||
});
|
||||
result.push(WrappedLine { text: String::new(), start_offset: 0 });
|
||||
}
|
||||
|
||||
result
|
||||
@@ -368,24 +378,28 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
||||
};
|
||||
|
||||
// Chat header с typing status
|
||||
let typing_action = app.td_client.typing_status.as_ref().map(|(_, action, _)| action.clone());
|
||||
let typing_action = app
|
||||
.td_client
|
||||
.typing_status
|
||||
.as_ref()
|
||||
.map(|(_, action, _)| action.clone());
|
||||
let header_line = if let Some(action) = typing_action {
|
||||
// Показываем typing status: "👤 Имя @username печатает..."
|
||||
let mut spans = vec![
|
||||
Span::styled(
|
||||
format!("👤 {}", chat.title),
|
||||
Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD),
|
||||
),
|
||||
];
|
||||
let mut spans = vec![Span::styled(
|
||||
format!("👤 {}", chat.title),
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.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),
|
||||
Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC),
|
||||
Style::default()
|
||||
.fg(Color::Yellow)
|
||||
.add_modifier(Modifier::ITALIC),
|
||||
));
|
||||
Line::from(spans)
|
||||
} else {
|
||||
@@ -396,17 +410,22 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
||||
};
|
||||
Line::from(Span::styled(
|
||||
header_text,
|
||||
Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD),
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
))
|
||||
};
|
||||
let header = Paragraph::new(header_line)
|
||||
.block(Block::default().borders(Borders::ALL));
|
||||
let header = Paragraph::new(header_line).block(Block::default().borders(Borders::ALL));
|
||||
f.render_widget(header, message_chunks[0]);
|
||||
|
||||
// Pinned bar (если есть закреплённое сообщение)
|
||||
if let Some(pinned_msg) = &app.td_client.current_pinned_message {
|
||||
let pinned_preview: String = pinned_msg.content.chars().take(40).collect();
|
||||
let ellipsis = if pinned_msg.content.chars().count() > 40 { "..." } else { "" };
|
||||
let ellipsis = if pinned_msg.content.chars().count() > 40 {
|
||||
"..."
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let pinned_datetime = crate::utils::format_datetime(pinned_msg.date);
|
||||
let pinned_text = format!("📌 {} {}{}", pinned_datetime, pinned_preview, ellipsis);
|
||||
let pinned_hint = "Ctrl+P";
|
||||
@@ -421,8 +440,8 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
||||
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, message_chunks[1]);
|
||||
}
|
||||
|
||||
@@ -484,9 +503,13 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
||||
}
|
||||
|
||||
let sender_style = if msg.is_outgoing {
|
||||
Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)
|
||||
Style::default()
|
||||
.fg(Color::Green)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
};
|
||||
|
||||
if msg.is_outgoing {
|
||||
@@ -540,16 +563,21 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
||||
]));
|
||||
} else {
|
||||
// Forward слева для входящих
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled(forward_line, Style::default().fg(Color::Magenta)),
|
||||
]));
|
||||
lines.push(Line::from(vec![Span::styled(
|
||||
forward_line,
|
||||
Style::default().fg(Color::Magenta),
|
||||
)]));
|
||||
}
|
||||
}
|
||||
|
||||
// Отображаем reply если есть
|
||||
if let Some(reply) = &msg.reply_to {
|
||||
let reply_text: String = reply.text.chars().take(40).collect();
|
||||
let ellipsis = if reply.text.chars().count() > 40 { "..." } else { "" };
|
||||
let ellipsis = if reply.text.chars().count() > 40 {
|
||||
"..."
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let reply_line = format!("┌ {}: {}{}", reply.sender_name, reply_text, ellipsis);
|
||||
let reply_len = reply_line.chars().count();
|
||||
|
||||
@@ -562,9 +590,10 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
||||
]));
|
||||
} else {
|
||||
// Reply слева для входящих
|
||||
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),
|
||||
)]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -593,11 +622,8 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
||||
);
|
||||
|
||||
// Форматируем текст с entities
|
||||
let formatted_spans = format_text_with_entities(
|
||||
&wrapped.text,
|
||||
&line_entities,
|
||||
msg_color,
|
||||
);
|
||||
let formatted_spans =
|
||||
format_text_with_entities(&wrapped.text, &line_entities, msg_color);
|
||||
|
||||
if is_last_line {
|
||||
// Последняя строка — добавляем time_mark
|
||||
@@ -605,17 +631,30 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
||||
let padding = content_width.saturating_sub(full_len + 1);
|
||||
let mut line_spans = vec![Span::raw(" ".repeat(padding))];
|
||||
if is_selected {
|
||||
line_spans.push(Span::styled(selection_marker, Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)));
|
||||
line_spans.push(Span::styled(
|
||||
selection_marker,
|
||||
Style::default()
|
||||
.fg(Color::Yellow)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
));
|
||||
}
|
||||
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);
|
||||
let mut line_spans = vec![Span::raw(" ".repeat(padding))];
|
||||
if i == 0 && is_selected {
|
||||
line_spans.push(Span::styled(selection_marker, Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)));
|
||||
line_spans.push(Span::styled(
|
||||
selection_marker,
|
||||
Style::default()
|
||||
.fg(Color::Yellow)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
));
|
||||
}
|
||||
line_spans.extend(formatted_spans);
|
||||
lines.push(Line::from(line_spans));
|
||||
@@ -643,19 +682,24 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
||||
);
|
||||
|
||||
// Форматируем текст с entities
|
||||
let formatted_spans = format_text_with_entities(
|
||||
&wrapped.text,
|
||||
&line_entities,
|
||||
msg_color,
|
||||
);
|
||||
let formatted_spans =
|
||||
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)));
|
||||
line_spans.push(Span::styled(
|
||||
selection_marker,
|
||||
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));
|
||||
@@ -694,9 +738,11 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
||||
};
|
||||
|
||||
let style = if reaction.is_chosen {
|
||||
Style::default().fg(app.config.parse_color(&app.config.colors.reaction_chosen))
|
||||
Style::default()
|
||||
.fg(app.config.parse_color(&app.config.colors.reaction_chosen))
|
||||
} else {
|
||||
Style::default().fg(app.config.parse_color(&app.config.colors.reaction_other))
|
||||
Style::default()
|
||||
.fg(app.config.parse_color(&app.config.colors.reaction_other))
|
||||
};
|
||||
|
||||
reaction_spans.push(Span::styled(reaction_text, style));
|
||||
@@ -723,10 +769,7 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
||||
}
|
||||
|
||||
if lines.is_empty() {
|
||||
lines.push(Line::from(Span::styled(
|
||||
"Нет сообщений",
|
||||
Style::default().fg(Color::Gray),
|
||||
)));
|
||||
lines.push(Line::from(Span::styled("Нет сообщений", Style::default().fg(Color::Gray))));
|
||||
}
|
||||
|
||||
// Вычисляем скролл с учётом пользовательского offset
|
||||
@@ -769,10 +812,15 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
||||
// Input box с wrap для длинного текста и блочным курсором
|
||||
let (input_line, input_title) = if app.is_forwarding() {
|
||||
// Режим пересылки - показываем превью сообщения
|
||||
let forward_preview = app.get_forwarding_message()
|
||||
let forward_preview = app
|
||||
.get_forwarding_message()
|
||||
.map(|m| {
|
||||
let text_preview: String = m.content.chars().take(40).collect();
|
||||
let ellipsis = if m.content.chars().count() > 40 { "..." } else { "" };
|
||||
let ellipsis = if m.content.chars().count() > 40 {
|
||||
"..."
|
||||
} else {
|
||||
""
|
||||
};
|
||||
format!("↪ {}{}", text_preview, ellipsis)
|
||||
})
|
||||
.unwrap_or_else(|| "↪ ...".to_string());
|
||||
@@ -782,8 +830,12 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
||||
} else if app.is_selecting_message() {
|
||||
// Режим выбора сообщения - подсказка зависит от возможностей
|
||||
let selected_msg = app.get_selected_message();
|
||||
let can_edit = selected_msg.map(|m| m.can_be_edited && m.is_outgoing).unwrap_or(false);
|
||||
let can_delete = selected_msg.map(|m| m.can_be_deleted_only_for_self || m.can_be_deleted_for_all_users).unwrap_or(false);
|
||||
let can_edit = selected_msg
|
||||
.map(|m| m.can_be_edited && m.is_outgoing)
|
||||
.unwrap_or(false);
|
||||
let can_delete = selected_msg
|
||||
.map(|m| m.can_be_deleted_only_for_self || m.can_be_deleted_for_all_users)
|
||||
.unwrap_or(false);
|
||||
|
||||
let hint = match (can_edit, can_delete) {
|
||||
(true, true) => "↑↓ · Enter ред. · r ответ · f перслть · y копир. · d удал. · Esc",
|
||||
@@ -791,7 +843,10 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
||||
(false, true) => "↑↓ · r ответ · f переслать · y копир. · d удалить · Esc",
|
||||
(false, false) => "↑↓ · r ответить · f переслать · y копировать · Esc",
|
||||
};
|
||||
(Line::from(Span::styled(hint, Style::default().fg(Color::Cyan))), " Выбор сообщения ")
|
||||
(
|
||||
Line::from(Span::styled(hint, Style::default().fg(Color::Cyan))),
|
||||
" Выбор сообщения ",
|
||||
)
|
||||
} else if app.is_editing() {
|
||||
// Режим редактирования
|
||||
if app.message_input.is_empty() {
|
||||
@@ -804,16 +859,30 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
||||
(line, " Редактирование (Esc отмена) ")
|
||||
} else {
|
||||
// Текст с курсором
|
||||
let line = render_input_with_cursor("✏ ", &app.message_input, app.cursor_position, Color::Magenta);
|
||||
let line = render_input_with_cursor(
|
||||
"✏ ",
|
||||
&app.message_input,
|
||||
app.cursor_position,
|
||||
Color::Magenta,
|
||||
);
|
||||
(line, " Редактирование (Esc отмена) ")
|
||||
}
|
||||
} else if app.is_replying() {
|
||||
// Режим ответа на сообщение
|
||||
let reply_preview = app.get_replying_to_message()
|
||||
let reply_preview = app
|
||||
.get_replying_to_message()
|
||||
.map(|m| {
|
||||
let sender = if m.is_outgoing { "Вы" } else { &m.sender_name };
|
||||
let sender = if m.is_outgoing {
|
||||
"Вы"
|
||||
} else {
|
||||
&m.sender_name
|
||||
};
|
||||
let text_preview: String = m.content.chars().take(30).collect();
|
||||
let ellipsis = if m.content.chars().count() > 30 { "..." } else { "" };
|
||||
let ellipsis = if m.content.chars().count() > 30 {
|
||||
"..."
|
||||
} else {
|
||||
""
|
||||
};
|
||||
format!("{}: {}{}", sender, text_preview, ellipsis)
|
||||
})
|
||||
.unwrap_or_else(|| "...".to_string());
|
||||
@@ -829,7 +898,12 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
||||
} else {
|
||||
let short_preview: String = reply_preview.chars().take(15).collect();
|
||||
let prefix = format!("↪ {} > ", short_preview);
|
||||
let line = render_input_with_cursor(&prefix, &app.message_input, app.cursor_position, Color::Yellow);
|
||||
let line = render_input_with_cursor(
|
||||
&prefix,
|
||||
&app.message_input,
|
||||
app.cursor_position,
|
||||
Color::Yellow,
|
||||
);
|
||||
(line, " Ответ (Esc отмена) ")
|
||||
}
|
||||
} else {
|
||||
@@ -844,7 +918,12 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
||||
(line, "")
|
||||
} else {
|
||||
// Текст с курсором
|
||||
let line = render_input_with_cursor("> ", &app.message_input, app.cursor_position, Color::Yellow);
|
||||
let line = render_input_with_cursor(
|
||||
"> ",
|
||||
&app.message_input,
|
||||
app.cursor_position,
|
||||
Color::Yellow,
|
||||
);
|
||||
(line, "")
|
||||
}
|
||||
};
|
||||
@@ -860,7 +939,11 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title(input_title)
|
||||
.title_style(Style::default().fg(title_color).add_modifier(Modifier::BOLD))
|
||||
.title_style(
|
||||
Style::default()
|
||||
.fg(title_color)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
)
|
||||
};
|
||||
|
||||
let input = Paragraph::new(input_line)
|
||||
@@ -882,7 +965,12 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
||||
|
||||
// Модалка выбора реакции
|
||||
if app.is_reaction_picker_mode() {
|
||||
render_reaction_picker_modal(f, area, &app.available_reactions, app.selected_reaction_index);
|
||||
render_reaction_picker_modal(
|
||||
f,
|
||||
area,
|
||||
&app.available_reactions,
|
||||
app.selected_reaction_index,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -899,8 +987,12 @@ fn render_search_mode(f: &mut Frame, area: Rect, app: &App) {
|
||||
|
||||
// Search input
|
||||
let total = app.message_search_results.len();
|
||||
let current = if total > 0 { app.selected_search_result_index + 1 } else { 0 };
|
||||
|
||||
let current = if total > 0 {
|
||||
app.selected_search_result_index + 1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let input_line = if app.message_search_query.is_empty() {
|
||||
Line::from(vec![
|
||||
Span::styled("🔍 ", Style::default().fg(Color::Yellow)),
|
||||
@@ -915,15 +1007,18 @@ fn render_search_mode(f: &mut Frame, area: Rect, app: &App) {
|
||||
Span::styled(format!(" ({}/{})", current, total), Style::default().fg(Color::Gray)),
|
||||
])
|
||||
};
|
||||
|
||||
let search_input = Paragraph::new(input_line)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(Color::Yellow))
|
||||
.title(" Поиск по сообщениям ")
|
||||
.title_style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD))
|
||||
);
|
||||
|
||||
let search_input = Paragraph::new(input_line).block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(Color::Yellow))
|
||||
.title(" Поиск по сообщениям ")
|
||||
.title_style(
|
||||
Style::default()
|
||||
.fg(Color::Yellow)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
);
|
||||
f.render_widget(search_input, chunks[0]);
|
||||
|
||||
// Search results
|
||||
@@ -948,14 +1043,29 @@ fn render_search_mode(f: &mut Frame, area: Rect, app: &App) {
|
||||
|
||||
// Маркер выбора, имя и дата
|
||||
let marker = if is_selected { "▶ " } else { " " };
|
||||
let sender_color = if msg.is_outgoing { Color::Green } else { Color::Cyan };
|
||||
let sender_name = if msg.is_outgoing { "Вы".to_string() } else { msg.sender_name.clone() };
|
||||
let sender_color = if msg.is_outgoing {
|
||||
Color::Green
|
||||
} else {
|
||||
Color::Cyan
|
||||
};
|
||||
let sender_name = if msg.is_outgoing {
|
||||
"Вы".to_string()
|
||||
} else {
|
||||
msg.sender_name.clone()
|
||||
};
|
||||
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled(marker, Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)),
|
||||
Span::styled(
|
||||
marker,
|
||||
Style::default()
|
||||
.fg(Color::Yellow)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::styled(
|
||||
format!("{} ", sender_name),
|
||||
Style::default().fg(sender_color).add_modifier(Modifier::BOLD),
|
||||
Style::default()
|
||||
.fg(sender_color)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::styled(
|
||||
format!("({})", crate::utils::format_datetime(msg.date)),
|
||||
@@ -964,7 +1074,11 @@ fn render_search_mode(f: &mut Frame, area: Rect, app: &App) {
|
||||
]));
|
||||
|
||||
// Текст сообщения (с переносом)
|
||||
let msg_color = if is_selected { Color::Yellow } else { Color::White };
|
||||
let msg_color = if is_selected {
|
||||
Color::Yellow
|
||||
} else {
|
||||
Color::White
|
||||
};
|
||||
let max_width = content_width.saturating_sub(4);
|
||||
let wrapped = wrap_text_with_offsets(&msg.content, max_width);
|
||||
let wrapped_count = wrapped.len();
|
||||
@@ -998,20 +1112,35 @@ fn render_search_mode(f: &mut Frame, area: Rect, app: &App) {
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(Color::Yellow))
|
||||
.border_style(Style::default().fg(Color::Yellow)),
|
||||
)
|
||||
.scroll((scroll_offset, 0));
|
||||
f.render_widget(results_widget, chunks[1]);
|
||||
|
||||
// Help bar
|
||||
let help_line = Line::from(vec![
|
||||
Span::styled(" ↑↓ ", Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)),
|
||||
Span::styled(
|
||||
" ↑↓ ",
|
||||
Style::default()
|
||||
.fg(Color::Yellow)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw("навигация"),
|
||||
Span::raw(" "),
|
||||
Span::styled(" n/N ", Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)),
|
||||
Span::styled(
|
||||
" n/N ",
|
||||
Style::default()
|
||||
.fg(Color::Yellow)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw("след./пред."),
|
||||
Span::raw(" "),
|
||||
Span::styled(" Enter ", Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)),
|
||||
Span::styled(
|
||||
" Enter ",
|
||||
Style::default()
|
||||
.fg(Color::Green)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw("перейти"),
|
||||
Span::raw(" "),
|
||||
Span::styled(" Esc ", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)),
|
||||
@@ -1021,7 +1150,7 @@ fn render_search_mode(f: &mut Frame, area: Rect, app: &App) {
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(Color::Yellow))
|
||||
.border_style(Style::default().fg(Color::Yellow)),
|
||||
)
|
||||
.alignment(Alignment::Center);
|
||||
f.render_widget(help, chunks[2]);
|
||||
@@ -1046,9 +1175,13 @@ fn render_pinned_mode(f: &mut Frame, area: Rect, app: &App) {
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(Color::Magenta))
|
||||
.border_style(Style::default().fg(Color::Magenta)),
|
||||
)
|
||||
.style(Style::default().fg(Color::Magenta).add_modifier(Modifier::BOLD));
|
||||
.style(
|
||||
Style::default()
|
||||
.fg(Color::Magenta)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
);
|
||||
f.render_widget(header, chunks[0]);
|
||||
|
||||
// Pinned messages list
|
||||
@@ -1057,7 +1190,7 @@ fn render_pinned_mode(f: &mut Frame, area: Rect, app: &App) {
|
||||
|
||||
for (idx, msg) in app.pinned_messages.iter().enumerate() {
|
||||
let is_selected = idx == app.selected_pinned_index;
|
||||
|
||||
|
||||
// Пустая строка между сообщениями
|
||||
if idx > 0 {
|
||||
lines.push(Line::from(""));
|
||||
@@ -1065,14 +1198,29 @@ fn render_pinned_mode(f: &mut Frame, area: Rect, app: &App) {
|
||||
|
||||
// Маркер выбора и имя отправителя
|
||||
let marker = if is_selected { "▶ " } else { " " };
|
||||
let sender_color = if msg.is_outgoing { Color::Green } else { Color::Cyan };
|
||||
let sender_name = if msg.is_outgoing { "Вы".to_string() } else { msg.sender_name.clone() };
|
||||
|
||||
let sender_color = if msg.is_outgoing {
|
||||
Color::Green
|
||||
} else {
|
||||
Color::Cyan
|
||||
};
|
||||
let sender_name = if msg.is_outgoing {
|
||||
"Вы".to_string()
|
||||
} else {
|
||||
msg.sender_name.clone()
|
||||
};
|
||||
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled(marker, Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)),
|
||||
Span::styled(
|
||||
marker,
|
||||
Style::default()
|
||||
.fg(Color::Yellow)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::styled(
|
||||
format!("{} ", sender_name),
|
||||
Style::default().fg(sender_color).add_modifier(Modifier::BOLD),
|
||||
Style::default()
|
||||
.fg(sender_color)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::styled(
|
||||
format!("({})", crate::utils::format_datetime(msg.date)),
|
||||
@@ -1081,12 +1229,17 @@ fn render_pinned_mode(f: &mut Frame, area: Rect, app: &App) {
|
||||
]));
|
||||
|
||||
// Текст сообщения (с переносом)
|
||||
let msg_color = if is_selected { Color::Yellow } else { Color::White };
|
||||
let msg_color = if is_selected {
|
||||
Color::Yellow
|
||||
} else {
|
||||
Color::White
|
||||
};
|
||||
let max_width = content_width.saturating_sub(4);
|
||||
let wrapped = wrap_text_with_offsets(&msg.content, max_width);
|
||||
let wrapped_count = wrapped.len();
|
||||
|
||||
for wrapped_line in wrapped.into_iter().take(3) { // Максимум 3 строки на сообщение
|
||||
|
||||
for wrapped_line in wrapped.into_iter().take(3) {
|
||||
// Максимум 3 строки на сообщение
|
||||
lines.push(Line::from(vec![
|
||||
Span::raw(" "), // Отступ
|
||||
Span::styled(wrapped_line.text, Style::default().fg(msg_color)),
|
||||
@@ -1121,17 +1274,27 @@ fn render_pinned_mode(f: &mut Frame, area: Rect, app: &App) {
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(Color::Magenta))
|
||||
.border_style(Style::default().fg(Color::Magenta)),
|
||||
)
|
||||
.scroll((scroll_offset, 0));
|
||||
f.render_widget(messages_widget, chunks[1]);
|
||||
|
||||
// Help bar
|
||||
let help_line = Line::from(vec![
|
||||
Span::styled(" ↑↓ ", Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)),
|
||||
Span::styled(
|
||||
" ↑↓ ",
|
||||
Style::default()
|
||||
.fg(Color::Yellow)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw("навигация"),
|
||||
Span::raw(" "),
|
||||
Span::styled(" Enter ", Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)),
|
||||
Span::styled(
|
||||
" Enter ",
|
||||
Style::default()
|
||||
.fg(Color::Green)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw("перейти"),
|
||||
Span::raw(" "),
|
||||
Span::styled(" Esc ", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)),
|
||||
@@ -1141,7 +1304,7 @@ fn render_pinned_mode(f: &mut Frame, area: Rect, app: &App) {
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(Color::Magenta))
|
||||
.border_style(Style::default().fg(Color::Magenta)),
|
||||
)
|
||||
.alignment(Alignment::Center);
|
||||
f.render_widget(help, chunks[2]);
|
||||
@@ -1169,11 +1332,18 @@ fn render_delete_confirm_modal(f: &mut Frame, area: Rect) {
|
||||
Line::from(""),
|
||||
Line::from(Span::styled(
|
||||
"Удалить сообщение?",
|
||||
Style::default().fg(Color::White).add_modifier(Modifier::BOLD),
|
||||
Style::default()
|
||||
.fg(Color::White)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
)),
|
||||
Line::from(""),
|
||||
Line::from(vec![
|
||||
Span::styled(" [y/Enter] ", Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)),
|
||||
Span::styled(
|
||||
" [y/Enter] ",
|
||||
Style::default()
|
||||
.fg(Color::Green)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw("Да"),
|
||||
Span::raw(" "),
|
||||
Span::styled(" [n/Esc] ", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)),
|
||||
@@ -1194,9 +1364,13 @@ fn render_delete_confirm_modal(f: &mut Frame, area: Rect) {
|
||||
f.render_widget(modal, modal_area);
|
||||
}
|
||||
|
||||
|
||||
/// Рендерит модалку выбора реакции
|
||||
fn render_reaction_picker_modal(f: &mut Frame, area: Rect, available_reactions: &[String], selected_index: usize) {
|
||||
fn render_reaction_picker_modal(
|
||||
f: &mut Frame,
|
||||
area: Rect,
|
||||
available_reactions: &[String],
|
||||
selected_index: usize,
|
||||
) {
|
||||
use ratatui::widgets::Clear;
|
||||
|
||||
// Размеры модалки (зависят от количества реакций)
|
||||
@@ -1248,9 +1422,19 @@ fn render_reaction_picker_modal(f: &mut Frame, area: Rect, available_reactions:
|
||||
// Добавляем пустую строку и подсказку
|
||||
text_lines.push(Line::from(""));
|
||||
text_lines.push(Line::from(vec![
|
||||
Span::styled(" [←/→/↑/↓] ", Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)),
|
||||
Span::styled(
|
||||
" [←/→/↑/↓] ",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw("Выбор "),
|
||||
Span::styled(" [Enter] ", Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)),
|
||||
Span::styled(
|
||||
" [Enter] ",
|
||||
Style::default()
|
||||
.fg(Color::Green)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw("Добавить "),
|
||||
Span::styled(" [Esc] ", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)),
|
||||
Span::raw("Отмена"),
|
||||
@@ -1262,7 +1446,11 @@ fn render_reaction_picker_modal(f: &mut Frame, area: Rect, available_reactions:
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(Color::Yellow))
|
||||
.title(" Выбери реакцию ")
|
||||
.title_style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)),
|
||||
.title_style(
|
||||
Style::default()
|
||||
.fg(Color::Yellow)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
)
|
||||
.alignment(Alignment::Left);
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
mod loading;
|
||||
mod auth;
|
||||
mod main_screen;
|
||||
pub mod chat_list;
|
||||
pub mod messages;
|
||||
pub mod footer;
|
||||
mod loading;
|
||||
mod main_screen;
|
||||
pub mod messages;
|
||||
pub mod profile;
|
||||
|
||||
use ratatui::Frame;
|
||||
use crate::app::{App, AppScreen};
|
||||
use ratatui::layout::Alignment;
|
||||
use ratatui::style::{Color, Modifier, Style};
|
||||
use ratatui::widgets::Paragraph;
|
||||
use crate::app::{App, AppScreen};
|
||||
use ratatui::Frame;
|
||||
|
||||
/// Минимальная высота терминала
|
||||
const MIN_HEIGHT: u16 = 10;
|
||||
@@ -34,12 +34,13 @@ pub fn render(f: &mut Frame, app: &mut App) {
|
||||
}
|
||||
|
||||
fn render_size_warning(f: &mut Frame, width: u16, height: u16) {
|
||||
let message = format!(
|
||||
"{}x{}\nМинимум: {}x{}",
|
||||
width, height, MIN_WIDTH, MIN_HEIGHT
|
||||
);
|
||||
let message = format!("{}x{}\nМинимум: {}x{}", width, height, MIN_WIDTH, MIN_HEIGHT);
|
||||
let warning = Paragraph::new(message)
|
||||
.style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD))
|
||||
.style(
|
||||
Style::default()
|
||||
.fg(Color::Yellow)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
)
|
||||
.alignment(Alignment::Center);
|
||||
f.render_widget(warning, f.area());
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use crate::app::App;
|
||||
use crate::tdlib::client::ProfileInfo;
|
||||
use ratatui::{
|
||||
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
@@ -5,8 +7,6 @@ use ratatui::{
|
||||
widgets::{Block, Borders, Paragraph},
|
||||
Frame,
|
||||
};
|
||||
use crate::app::App;
|
||||
use crate::tdlib::client::ProfileInfo;
|
||||
|
||||
/// Рендерит режим просмотра профиля
|
||||
pub fn render(f: &mut Frame, area: Rect, app: &App, profile: &ProfileInfo) {
|
||||
@@ -20,9 +20,9 @@ pub fn render(f: &mut Frame, area: Rect, app: &App, profile: &ProfileInfo) {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Length(3), // Header
|
||||
Constraint::Min(0), // Profile info
|
||||
Constraint::Length(3), // Actions help
|
||||
Constraint::Length(3), // Header
|
||||
Constraint::Min(0), // Profile info
|
||||
Constraint::Length(3), // Actions help
|
||||
])
|
||||
.split(area);
|
||||
|
||||
@@ -32,9 +32,13 @@ pub fn render(f: &mut Frame, area: Rect, app: &App, profile: &ProfileInfo) {
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(Color::Cyan))
|
||||
.border_style(Style::default().fg(Color::Cyan)),
|
||||
)
|
||||
.style(Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD));
|
||||
.style(
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
);
|
||||
f.render_widget(header, chunks[0]);
|
||||
|
||||
// Profile info
|
||||
@@ -83,9 +87,7 @@ pub fn render(f: &mut Frame, area: Rect, app: &App, profile: &ProfileInfo) {
|
||||
|
||||
// Bio (только для личных чатов)
|
||||
if let Some(bio) = &profile.bio {
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled("О себе: ", Style::default().fg(Color::Gray)),
|
||||
]));
|
||||
lines.push(Line::from(vec![Span::styled("О себе: ", Style::default().fg(Color::Gray))]));
|
||||
// Разбиваем bio на строки если длинное
|
||||
let bio_lines: Vec<&str> = bio.lines().collect();
|
||||
for bio_line in bio_lines {
|
||||
@@ -105,9 +107,7 @@ pub fn render(f: &mut Frame, area: Rect, app: &App, profile: &ProfileInfo) {
|
||||
|
||||
// Description (для групп/каналов)
|
||||
if let Some(desc) = &profile.description {
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled("Описание: ", Style::default().fg(Color::Gray)),
|
||||
]));
|
||||
lines.push(Line::from(vec![Span::styled("Описание: ", Style::default().fg(Color::Gray))]));
|
||||
let desc_lines: Vec<&str> = desc.lines().collect();
|
||||
for desc_line in desc_lines {
|
||||
lines.push(Line::from(Span::styled(desc_line, Style::default().fg(Color::White))));
|
||||
@@ -119,7 +119,12 @@ pub fn render(f: &mut Frame, area: Rect, app: &App, profile: &ProfileInfo) {
|
||||
if let Some(link) = &profile.invite_link {
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled("Ссылка: ", Style::default().fg(Color::Gray)),
|
||||
Span::styled(link, Style::default().fg(Color::Blue).add_modifier(Modifier::UNDERLINED)),
|
||||
Span::styled(
|
||||
link,
|
||||
Style::default()
|
||||
.fg(Color::Blue)
|
||||
.add_modifier(Modifier::UNDERLINED),
|
||||
),
|
||||
]));
|
||||
lines.push(Line::from(""));
|
||||
}
|
||||
@@ -131,7 +136,9 @@ pub fn render(f: &mut Frame, area: Rect, app: &App, profile: &ProfileInfo) {
|
||||
// Действия
|
||||
lines.push(Line::from(Span::styled(
|
||||
"Действия:",
|
||||
Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD),
|
||||
Style::default()
|
||||
.fg(Color::Yellow)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
)));
|
||||
lines.push(Line::from(""));
|
||||
|
||||
@@ -140,7 +147,9 @@ pub fn render(f: &mut Frame, area: Rect, app: &App, profile: &ProfileInfo) {
|
||||
let is_selected = idx == app.selected_profile_action;
|
||||
let marker = if is_selected { "▶ " } else { " " };
|
||||
let style = if is_selected {
|
||||
Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)
|
||||
Style::default()
|
||||
.fg(Color::Yellow)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
Style::default().fg(Color::White)
|
||||
};
|
||||
@@ -154,17 +163,27 @@ pub fn render(f: &mut Frame, area: Rect, app: &App, profile: &ProfileInfo) {
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(Color::Cyan))
|
||||
.border_style(Style::default().fg(Color::Cyan)),
|
||||
)
|
||||
.scroll((0, 0));
|
||||
f.render_widget(info_widget, chunks[1]);
|
||||
|
||||
// Help bar
|
||||
let help_line = Line::from(vec![
|
||||
Span::styled(" ↑↓ ", Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)),
|
||||
Span::styled(
|
||||
" ↑↓ ",
|
||||
Style::default()
|
||||
.fg(Color::Yellow)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw("навигация"),
|
||||
Span::raw(" "),
|
||||
Span::styled(" Enter ", Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)),
|
||||
Span::styled(
|
||||
" Enter ",
|
||||
Style::default()
|
||||
.fg(Color::Green)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw("выбрать"),
|
||||
Span::raw(" "),
|
||||
Span::styled(" Esc ", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)),
|
||||
@@ -174,7 +193,7 @@ pub fn render(f: &mut Frame, area: Rect, app: &App, profile: &ProfileInfo) {
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(Color::Cyan))
|
||||
.border_style(Style::default().fg(Color::Cyan)),
|
||||
)
|
||||
.alignment(Alignment::Center);
|
||||
f.render_widget(help, chunks[2]);
|
||||
@@ -183,17 +202,17 @@ pub fn render(f: &mut Frame, area: Rect, app: &App, profile: &ProfileInfo) {
|
||||
/// Получить список доступных действий
|
||||
fn get_available_actions(profile: &ProfileInfo) -> Vec<&'static str> {
|
||||
let mut actions = vec![];
|
||||
|
||||
|
||||
if profile.username.is_some() {
|
||||
actions.push("Открыть в браузере");
|
||||
}
|
||||
|
||||
|
||||
actions.push("Скопировать ID");
|
||||
|
||||
|
||||
if profile.is_group {
|
||||
actions.push("Покинуть группу");
|
||||
}
|
||||
|
||||
|
||||
actions
|
||||
}
|
||||
|
||||
@@ -212,12 +231,19 @@ fn render_leave_confirmation_modal(f: &mut Frame, area: Rect, step: u8) {
|
||||
Line::from(""),
|
||||
Line::from(Span::styled(
|
||||
text,
|
||||
Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD),
|
||||
Style::default()
|
||||
.fg(Color::Yellow)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
)),
|
||||
Line::from(""),
|
||||
Line::from(""),
|
||||
Line::from(vec![
|
||||
Span::styled("y/н/Enter", Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)),
|
||||
Span::styled(
|
||||
"y/н/Enter",
|
||||
Style::default()
|
||||
.fg(Color::Green)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" — да "),
|
||||
Span::styled("n/т/Esc", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)),
|
||||
Span::raw(" — нет"),
|
||||
@@ -230,7 +256,7 @@ fn render_leave_confirmation_modal(f: &mut Frame, area: Rect, step: u8) {
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(Color::Red))
|
||||
.title(" ⚠ ВНИМАНИЕ ")
|
||||
.title_style(Style::default().fg(Color::Red).add_modifier(Modifier::BOLD))
|
||||
.title_style(Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)),
|
||||
)
|
||||
.alignment(Alignment::Center);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user