add account profile
This commit is contained in:
@@ -66,6 +66,15 @@ pub struct App {
|
||||
pub message_search_results: Vec<crate::tdlib::client::MessageInfo>,
|
||||
/// Индекс выбранного результата
|
||||
pub selected_search_result_index: usize,
|
||||
// Profile mode
|
||||
/// Режим просмотра профиля
|
||||
pub is_profile_mode: bool,
|
||||
/// Индекс выбранного действия в профиле
|
||||
pub selected_profile_action: usize,
|
||||
/// Шаг подтверждения выхода из группы (0 = не показано, 1 = первое, 2 = второе)
|
||||
pub leave_group_confirmation_step: u8,
|
||||
/// Информация профиля для отображения
|
||||
pub profile_info: Option<crate::tdlib::ProfileInfo>,
|
||||
}
|
||||
|
||||
impl App {
|
||||
@@ -106,6 +115,10 @@ impl App {
|
||||
message_search_query: String::new(),
|
||||
message_search_results: Vec::new(),
|
||||
selected_search_result_index: 0,
|
||||
is_profile_mode: false,
|
||||
selected_profile_action: 0,
|
||||
leave_group_confirmation_step: 0,
|
||||
profile_info: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -537,4 +550,60 @@ impl App {
|
||||
self.cursor_position = self.message_input.chars().count();
|
||||
}
|
||||
}
|
||||
|
||||
// === Profile Mode ===
|
||||
|
||||
/// Проверить, активен ли режим профиля
|
||||
pub fn is_profile_mode(&self) -> bool {
|
||||
self.is_profile_mode
|
||||
}
|
||||
|
||||
/// Войти в режим профиля
|
||||
pub fn enter_profile_mode(&mut self) {
|
||||
self.is_profile_mode = true;
|
||||
self.selected_profile_action = 0;
|
||||
self.leave_group_confirmation_step = 0;
|
||||
}
|
||||
|
||||
/// Выйти из режима профиля
|
||||
pub fn exit_profile_mode(&mut self) {
|
||||
self.is_profile_mode = false;
|
||||
self.selected_profile_action = 0;
|
||||
self.leave_group_confirmation_step = 0;
|
||||
self.profile_info = None;
|
||||
}
|
||||
|
||||
/// Выбрать предыдущее действие
|
||||
pub fn select_previous_profile_action(&mut self) {
|
||||
if self.selected_profile_action > 0 {
|
||||
self.selected_profile_action -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Выбрать следующее действие
|
||||
pub fn select_next_profile_action(&mut self, max_actions: usize) {
|
||||
if self.selected_profile_action < max_actions.saturating_sub(1) {
|
||||
self.selected_profile_action += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Показать первое подтверждение выхода из группы
|
||||
pub fn show_leave_group_confirmation(&mut self) {
|
||||
self.leave_group_confirmation_step = 1;
|
||||
}
|
||||
|
||||
/// Показать второе подтверждение выхода из группы
|
||||
pub fn show_leave_group_final_confirmation(&mut self) {
|
||||
self.leave_group_confirmation_step = 2;
|
||||
}
|
||||
|
||||
/// Отменить подтверждение выхода из группы
|
||||
pub fn cancel_leave_group(&mut self) {
|
||||
self.leave_group_confirmation_step = 0;
|
||||
}
|
||||
|
||||
/// Получить текущий шаг подтверждения
|
||||
pub fn get_leave_group_confirmation_step(&self) -> u8 {
|
||||
self.leave_group_confirmation_step
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,9 +56,109 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Режим профиля
|
||||
if app.is_profile_mode() {
|
||||
// Обработка подтверждения выхода из группы
|
||||
let confirmation_step = app.get_leave_group_confirmation_step();
|
||||
if confirmation_step > 0 {
|
||||
match key.code {
|
||||
KeyCode::Char('y') | KeyCode::Char('н') | KeyCode::Enter => {
|
||||
if confirmation_step == 1 {
|
||||
// Первое подтверждение - показываем второе
|
||||
app.show_leave_group_final_confirmation();
|
||||
} else if confirmation_step == 2 {
|
||||
// Второе подтверждение - выходим из группы
|
||||
if let Some(chat_id) = app.selected_chat_id {
|
||||
let leave_result = app.td_client.leave_chat(chat_id).await;
|
||||
match leave_result {
|
||||
Ok(_) => {
|
||||
app.status_message = Some("Вы вышли из группы".to_string());
|
||||
app.exit_profile_mode();
|
||||
app.close_chat();
|
||||
}
|
||||
Err(e) => {
|
||||
app.error_message = Some(e);
|
||||
app.cancel_leave_group();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyCode::Char('n') | KeyCode::Char('т') | KeyCode::Esc => {
|
||||
// Отмена
|
||||
app.cancel_leave_group();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Обычная навигация по профилю
|
||||
match key.code {
|
||||
KeyCode::Esc => {
|
||||
app.exit_profile_mode();
|
||||
}
|
||||
KeyCode::Up => {
|
||||
app.select_previous_profile_action();
|
||||
}
|
||||
KeyCode::Down => {
|
||||
if let Some(profile) = &app.profile_info {
|
||||
let max_actions = get_available_actions_count(profile);
|
||||
app.select_next_profile_action(max_actions);
|
||||
}
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
// Выполнить выбранное действие
|
||||
if let Some(profile) = &app.profile_info {
|
||||
let actions = get_available_actions_count(profile);
|
||||
let action_index = app.selected_profile_action;
|
||||
|
||||
if action_index < actions {
|
||||
// Определяем какое действие выбрано
|
||||
let mut current_idx = 0;
|
||||
|
||||
// Действие: Открыть в браузере
|
||||
if profile.username.is_some() {
|
||||
if action_index == current_idx {
|
||||
if let Some(username) = &profile.username {
|
||||
let url = format!("https://t.me/{}", username.trim_start_matches('@'));
|
||||
match open::that(&url) {
|
||||
Ok(_) => {
|
||||
app.status_message = Some(format!("Открыто: {}", url));
|
||||
}
|
||||
Err(e) => {
|
||||
app.error_message = Some(format!("Ошибка открытия браузера: {}", e));
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
current_idx += 1;
|
||||
}
|
||||
|
||||
// Действие: Скопировать ID
|
||||
if action_index == current_idx {
|
||||
app.status_message = Some(format!("ID скопирован: {}", profile.chat_id));
|
||||
return;
|
||||
}
|
||||
current_idx += 1;
|
||||
|
||||
// Действие: Покинуть группу
|
||||
if profile.is_group && action_index == current_idx {
|
||||
app.show_leave_group_confirmation();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Режим поиска по сообщениям
|
||||
if app.is_message_search_mode() {
|
||||
match key.code {
|
||||
@@ -468,6 +568,29 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ctrl+U для профиля
|
||||
if key.code == KeyCode::Char('u') && has_ctrl {
|
||||
if let Some(chat_id) = app.selected_chat_id {
|
||||
app.status_message = Some("Загрузка профиля...".to_string());
|
||||
match timeout(Duration::from_secs(5), app.td_client.get_profile_info(chat_id)).await {
|
||||
Ok(Ok(profile)) => {
|
||||
app.profile_info = Some(profile);
|
||||
app.enter_profile_mode();
|
||||
app.status_message = None;
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
app.error_message = Some(e);
|
||||
app.status_message = None;
|
||||
}
|
||||
Err(_) => {
|
||||
app.error_message = Some("Таймаут загрузки профиля".to_string());
|
||||
app.status_message = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
match key.code {
|
||||
KeyCode::Backspace => {
|
||||
// Удаляем символ слева от курсора
|
||||
@@ -616,3 +739,20 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Подсчёт количества доступных действий в профиле
|
||||
fn get_available_actions_count(profile: &crate::tdlib::ProfileInfo) -> usize {
|
||||
let mut count = 0;
|
||||
|
||||
if profile.username.is_some() {
|
||||
count += 1; // Открыть в браузере
|
||||
}
|
||||
|
||||
count += 1; // Скопировать ID
|
||||
|
||||
if profile.is_group {
|
||||
count += 1; // Покинуть группу
|
||||
}
|
||||
|
||||
count
|
||||
}
|
||||
|
||||
@@ -167,6 +167,22 @@ pub struct FolderInfo {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
/// Информация о профиле чата/пользователя
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProfileInfo {
|
||||
pub chat_id: i64,
|
||||
pub title: String,
|
||||
pub username: Option<String>,
|
||||
pub bio: Option<String>,
|
||||
pub phone_number: Option<String>,
|
||||
pub chat_type: String, // "Личный чат", "Группа", "Канал"
|
||||
pub member_count: Option<i32>,
|
||||
pub description: Option<String>,
|
||||
pub invite_link: Option<String>,
|
||||
pub is_group: bool,
|
||||
pub online_status: Option<String>,
|
||||
}
|
||||
|
||||
/// Состояние сетевого соединения
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum NetworkState {
|
||||
@@ -1213,6 +1229,137 @@ impl TdClient {
|
||||
}
|
||||
}
|
||||
|
||||
/// Получение полной информации о чате для профиля
|
||||
pub async fn get_profile_info(&self, chat_id: i64) -> Result<ProfileInfo, String> {
|
||||
use tdlib_rs::enums::ChatType;
|
||||
|
||||
// Получаем основную информацию о чате
|
||||
let chat_result = functions::get_chat(chat_id, self.client_id).await;
|
||||
let chat = match chat_result {
|
||||
Ok(tdlib_rs::enums::Chat::Chat(c)) => c,
|
||||
Err(e) => return Err(format!("Ошибка загрузки чата: {:?}", e)),
|
||||
};
|
||||
|
||||
let mut profile = ProfileInfo {
|
||||
chat_id,
|
||||
title: chat.title.clone(),
|
||||
username: None,
|
||||
bio: None,
|
||||
phone_number: None,
|
||||
chat_type: String::new(),
|
||||
member_count: None,
|
||||
description: None,
|
||||
invite_link: None,
|
||||
is_group: false,
|
||||
online_status: None,
|
||||
};
|
||||
|
||||
match &chat.r#type {
|
||||
ChatType::Private(private_chat) => {
|
||||
profile.chat_type = "Личный чат".to_string();
|
||||
profile.is_group = false;
|
||||
|
||||
// Получаем полную информацию о пользователе
|
||||
let user_result = functions::get_user(private_chat.user_id, self.client_id).await;
|
||||
if let Ok(tdlib_rs::enums::User::User(user)) = user_result {
|
||||
// Username
|
||||
if let Some(usernames) = user.usernames {
|
||||
if let Some(username) = usernames.active_usernames.first() {
|
||||
profile.username = Some(format!("@{}", username));
|
||||
}
|
||||
}
|
||||
|
||||
// Phone number
|
||||
if !user.phone_number.is_empty() {
|
||||
profile.phone_number = Some(format!("+{}", user.phone_number));
|
||||
}
|
||||
|
||||
// Online status
|
||||
profile.online_status = Some(match user.status {
|
||||
tdlib_rs::enums::UserStatus::Online(_) => "Онлайн".to_string(),
|
||||
tdlib_rs::enums::UserStatus::Recently(_) => "Был(а) недавно".to_string(),
|
||||
tdlib_rs::enums::UserStatus::LastWeek(_) => "Был(а) на этой неделе".to_string(),
|
||||
tdlib_rs::enums::UserStatus::LastMonth(_) => "Был(а) в этом месяце".to_string(),
|
||||
tdlib_rs::enums::UserStatus::Offline(offline) => {
|
||||
crate::utils::format_was_online(offline.was_online)
|
||||
}
|
||||
_ => "Давно не был(а)".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
// Bio (getUserFullInfo)
|
||||
let full_info_result = functions::get_user_full_info(private_chat.user_id, self.client_id).await;
|
||||
if let Ok(tdlib_rs::enums::UserFullInfo::UserFullInfo(full_info)) = full_info_result {
|
||||
if let Some(bio_obj) = full_info.bio {
|
||||
profile.bio = Some(bio_obj.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
ChatType::BasicGroup(basic_group) => {
|
||||
profile.chat_type = "Группа".to_string();
|
||||
profile.is_group = true;
|
||||
|
||||
// Получаем информацию о группе
|
||||
let group_result = functions::get_basic_group(basic_group.basic_group_id, self.client_id).await;
|
||||
if let Ok(tdlib_rs::enums::BasicGroup::BasicGroup(group)) = group_result {
|
||||
profile.member_count = Some(group.member_count);
|
||||
}
|
||||
|
||||
// Полная информация о группе
|
||||
let full_info_result = functions::get_basic_group_full_info(basic_group.basic_group_id, self.client_id).await;
|
||||
if let Ok(tdlib_rs::enums::BasicGroupFullInfo::BasicGroupFullInfo(full_info)) = full_info_result {
|
||||
if !full_info.description.is_empty() {
|
||||
profile.description = Some(full_info.description);
|
||||
}
|
||||
if let Some(link) = full_info.invite_link {
|
||||
profile.invite_link = Some(link.invite_link);
|
||||
}
|
||||
}
|
||||
}
|
||||
ChatType::Supergroup(supergroup) => {
|
||||
// Получаем информацию о супергруппе
|
||||
let sg_result = functions::get_supergroup(supergroup.supergroup_id, self.client_id).await;
|
||||
if let Ok(tdlib_rs::enums::Supergroup::Supergroup(sg)) = sg_result {
|
||||
profile.chat_type = if sg.is_channel { "Канал".to_string() } else { "Супергруппа".to_string() };
|
||||
profile.is_group = !sg.is_channel;
|
||||
profile.member_count = Some(sg.member_count);
|
||||
|
||||
// Username
|
||||
if let Some(usernames) = sg.usernames {
|
||||
if let Some(username) = usernames.active_usernames.first() {
|
||||
profile.username = Some(format!("@{}", username));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Полная информация о супергруппе
|
||||
let full_info_result = functions::get_supergroup_full_info(supergroup.supergroup_id, self.client_id).await;
|
||||
if let Ok(tdlib_rs::enums::SupergroupFullInfo::SupergroupFullInfo(full_info)) = full_info_result {
|
||||
if !full_info.description.is_empty() {
|
||||
profile.description = Some(full_info.description);
|
||||
}
|
||||
if let Some(link) = full_info.invite_link {
|
||||
profile.invite_link = Some(link.invite_link);
|
||||
}
|
||||
}
|
||||
}
|
||||
ChatType::Secret(_) => {
|
||||
profile.chat_type = "Секретный чат".to_string();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(profile)
|
||||
}
|
||||
|
||||
/// Выйти из группы/канала
|
||||
pub async fn leave_chat(&self, chat_id: i64) -> Result<(), String> {
|
||||
let result = functions::leave_chat(chat_id, self.client_id).await;
|
||||
match result {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => Err(format!("Ошибка выхода из чата: {:?}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Загрузка старых сообщений (для скролла вверх)
|
||||
pub async fn load_older_messages(
|
||||
&mut self,
|
||||
|
||||
@@ -3,4 +3,5 @@ pub mod client;
|
||||
pub use client::TdClient;
|
||||
pub use client::UserOnlineStatus;
|
||||
pub use client::NetworkState;
|
||||
pub use client::ProfileInfo;
|
||||
pub use tdlib_rs::enums::ChatAction;
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
28
src/utils.rs
28
src/utils.rs
@@ -108,3 +108,31 @@ pub fn format_datetime(timestamp: i32) -> String {
|
||||
|
||||
format!("{:02}.{:02}.{} {:02}:{:02}", day + 1, month, year, local_hours, minutes)
|
||||
}
|
||||
|
||||
/// Форматирование "был(а) онлайн" из timestamp
|
||||
pub 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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user