add account profile
This commit is contained in:
@@ -157,28 +157,5 @@ pub fn render(f: &mut Frame, area: Rect, app: &mut App) {
|
||||
|
||||
/// Форматирование времени "был(а) в ..."
|
||||
fn format_was_online(timestamp: i32) -> String {
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs() as i32;
|
||||
|
||||
let diff = now - timestamp;
|
||||
|
||||
if diff < 60 {
|
||||
"был(а) только что".to_string()
|
||||
} else if diff < 3600 {
|
||||
let mins = diff / 60;
|
||||
format!("был(а) {} мин. назад", mins)
|
||||
} else if diff < 86400 {
|
||||
let hours = diff / 3600;
|
||||
format!("был(а) {} ч. назад", hours)
|
||||
} else {
|
||||
// Показываем дату
|
||||
let datetime = chrono::DateTime::from_timestamp(timestamp as i64, 0)
|
||||
.map(|dt| dt.format("%d.%m %H:%M").to_string())
|
||||
.unwrap_or_else(|| "давно".to_string());
|
||||
format!("был(а) {}", datetime)
|
||||
}
|
||||
crate::utils::format_was_online(timestamp)
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
||||
} else if app.is_searching {
|
||||
format!(" {}↑/↓: Navigate | Enter: Select | Esc: Cancel ", network_indicator)
|
||||
} else if app.selected_chat_id.is_some() {
|
||||
format!(" {}↑/↓: Scroll | Enter: Send | Esc: Close | Ctrl+R: Refresh | Ctrl+C: Quit ", network_indicator)
|
||||
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)
|
||||
};
|
||||
|
||||
@@ -310,6 +310,14 @@ fn adjust_entities_for_substring(
|
||||
}
|
||||
|
||||
pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
||||
// Режим профиля
|
||||
if app.is_profile_mode() {
|
||||
if let Some(profile) = &app.profile_info {
|
||||
crate::ui::profile::render(f, area, app, profile);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Режим поиска по сообщениям
|
||||
if app.is_message_search_mode() {
|
||||
render_search_mode(f, area, app);
|
||||
|
||||
@@ -4,6 +4,7 @@ mod main_screen;
|
||||
mod chat_list;
|
||||
mod messages;
|
||||
mod footer;
|
||||
pub mod profile;
|
||||
|
||||
use ratatui::Frame;
|
||||
use ratatui::layout::Alignment;
|
||||
|
||||
259
src/ui/profile.rs
Normal file
259
src/ui/profile.rs
Normal file
@@ -0,0 +1,259 @@
|
||||
use ratatui::{
|
||||
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Line, Span},
|
||||
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) {
|
||||
// Проверяем, показывать ли модалку подтверждения
|
||||
let confirmation_step = app.get_leave_group_confirmation_step();
|
||||
if confirmation_step > 0 {
|
||||
render_leave_confirmation_modal(f, area, confirmation_step);
|
||||
return;
|
||||
}
|
||||
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Length(3), // Header
|
||||
Constraint::Min(0), // Profile info
|
||||
Constraint::Length(3), // Actions help
|
||||
])
|
||||
.split(area);
|
||||
|
||||
// Header
|
||||
let header_text = format!("👤 ПРОФИЛЬ: {}", profile.title);
|
||||
let header = Paragraph::new(header_text)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(Color::Cyan))
|
||||
)
|
||||
.style(Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD));
|
||||
f.render_widget(header, chunks[0]);
|
||||
|
||||
// Profile info
|
||||
let mut lines: Vec<Line> = Vec::new();
|
||||
|
||||
// Тип чата
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled("Тип: ", Style::default().fg(Color::Gray)),
|
||||
Span::styled(&profile.chat_type, Style::default().fg(Color::White)),
|
||||
]));
|
||||
lines.push(Line::from(""));
|
||||
|
||||
// ID
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled("ID: ", Style::default().fg(Color::Gray)),
|
||||
Span::styled(format!("{}", profile.chat_id), Style::default().fg(Color::White)),
|
||||
]));
|
||||
lines.push(Line::from(""));
|
||||
|
||||
// Username
|
||||
if let Some(username) = &profile.username {
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled("Username: ", Style::default().fg(Color::Gray)),
|
||||
Span::styled(username, Style::default().fg(Color::Cyan)),
|
||||
]));
|
||||
lines.push(Line::from(""));
|
||||
}
|
||||
|
||||
// Phone number (только для личных чатов)
|
||||
if let Some(phone) = &profile.phone_number {
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled("Телефон: ", Style::default().fg(Color::Gray)),
|
||||
Span::styled(phone, Style::default().fg(Color::White)),
|
||||
]));
|
||||
lines.push(Line::from(""));
|
||||
}
|
||||
|
||||
// Online status (только для личных чатов)
|
||||
if let Some(status) = &profile.online_status {
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled("Статус: ", Style::default().fg(Color::Gray)),
|
||||
Span::styled(status, Style::default().fg(Color::Green)),
|
||||
]));
|
||||
lines.push(Line::from(""));
|
||||
}
|
||||
|
||||
// Bio (только для личных чатов)
|
||||
if let Some(bio) = &profile.bio {
|
||||
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 {
|
||||
lines.push(Line::from(Span::styled(bio_line, Style::default().fg(Color::White))));
|
||||
}
|
||||
lines.push(Line::from(""));
|
||||
}
|
||||
|
||||
// Member count (для групп/каналов)
|
||||
if let Some(count) = profile.member_count {
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled("Участников: ", Style::default().fg(Color::Gray)),
|
||||
Span::styled(format!("{}", count), Style::default().fg(Color::White)),
|
||||
]));
|
||||
lines.push(Line::from(""));
|
||||
}
|
||||
|
||||
// Description (для групп/каналов)
|
||||
if let Some(desc) = &profile.description {
|
||||
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))));
|
||||
}
|
||||
lines.push(Line::from(""));
|
||||
}
|
||||
|
||||
// Invite link (для групп/каналов)
|
||||
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)),
|
||||
]));
|
||||
lines.push(Line::from(""));
|
||||
}
|
||||
|
||||
// Разделитель
|
||||
lines.push(Line::from("────────────────────────────────"));
|
||||
lines.push(Line::from(""));
|
||||
|
||||
// Действия
|
||||
lines.push(Line::from(Span::styled(
|
||||
"Действия:",
|
||||
Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD),
|
||||
)));
|
||||
lines.push(Line::from(""));
|
||||
|
||||
let actions = get_available_actions(profile);
|
||||
for (idx, action) in actions.iter().enumerate() {
|
||||
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)
|
||||
} else {
|
||||
Style::default().fg(Color::White)
|
||||
};
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled(marker, Style::default().fg(Color::Yellow)),
|
||||
Span::styled(*action, style),
|
||||
]));
|
||||
}
|
||||
|
||||
let info_widget = Paragraph::new(lines)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.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::raw("навигация"),
|
||||
Span::raw(" "),
|
||||
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)),
|
||||
Span::raw("выход"),
|
||||
]);
|
||||
let help = Paragraph::new(help_line)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(Color::Cyan))
|
||||
)
|
||||
.alignment(Alignment::Center);
|
||||
f.render_widget(help, chunks[2]);
|
||||
}
|
||||
|
||||
/// Получить список доступных действий
|
||||
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
|
||||
}
|
||||
|
||||
/// Рендерит модалку подтверждения выхода из группы
|
||||
fn render_leave_confirmation_modal(f: &mut Frame, area: Rect, step: u8) {
|
||||
// Затемняем фон
|
||||
let modal_area = centered_rect(60, 30, area);
|
||||
|
||||
let text = if step == 1 {
|
||||
"Вы хотите выйти из группы?"
|
||||
} else {
|
||||
"Вы ТОЧНО хотите выйти из группы?!?!?"
|
||||
};
|
||||
|
||||
let lines = vec![
|
||||
Line::from(""),
|
||||
Line::from(Span::styled(
|
||||
text,
|
||||
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::raw(" — да "),
|
||||
Span::styled("n/т/Esc", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)),
|
||||
Span::raw(" — нет"),
|
||||
]),
|
||||
];
|
||||
|
||||
let modal = Paragraph::new(lines)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(Color::Red))
|
||||
.title(" ⚠ ВНИМАНИЕ ")
|
||||
.title_style(Style::default().fg(Color::Red).add_modifier(Modifier::BOLD))
|
||||
)
|
||||
.alignment(Alignment::Center);
|
||||
|
||||
f.render_widget(modal, modal_area);
|
||||
}
|
||||
|
||||
/// Вспомогательная функция для центрирования прямоугольника
|
||||
fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
|
||||
let popup_layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Percentage((100 - percent_y) / 2),
|
||||
Constraint::Percentage(percent_y),
|
||||
Constraint::Percentage((100 - percent_y) / 2),
|
||||
])
|
||||
.split(r);
|
||||
|
||||
Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([
|
||||
Constraint::Percentage((100 - percent_x) / 2),
|
||||
Constraint::Percentage(percent_x),
|
||||
Constraint::Percentage((100 - percent_x) / 2),
|
||||
])
|
||||
.split(popup_layout[1])[1]
|
||||
}
|
||||
Reference in New Issue
Block a user