yet-another-changes #10
1
.serena/.gitignore
vendored
Normal file
1
.serena/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/cache
|
||||||
89
.serena/project.yml
Normal file
89
.serena/project.yml
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
# list of languages for which language servers are started; choose from:
|
||||||
|
# al bash clojure cpp csharp
|
||||||
|
# csharp_omnisharp dart elixir elm erlang
|
||||||
|
# fortran fsharp go groovy haskell
|
||||||
|
# java julia kotlin lua markdown
|
||||||
|
# matlab nix pascal perl php
|
||||||
|
# powershell python python_jedi r rego
|
||||||
|
# ruby ruby_solargraph rust scala swift
|
||||||
|
# terraform toml typescript typescript_vts vue
|
||||||
|
# yaml zig
|
||||||
|
# (This list may be outdated. For the current list, see values of Language enum here:
|
||||||
|
# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py
|
||||||
|
# For some languages, there are alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.)
|
||||||
|
# Note:
|
||||||
|
# - For C, use cpp
|
||||||
|
# - For JavaScript, use typescript
|
||||||
|
# - For Free Pascal/Lazarus, use pascal
|
||||||
|
# Special requirements:
|
||||||
|
# - csharp: Requires the presence of a .sln file in the project folder.
|
||||||
|
# - pascal: Requires Free Pascal Compiler (fpc) and optionally Lazarus.
|
||||||
|
# When using multiple languages, the first language server that supports a given file will be used for that file.
|
||||||
|
# The first language is the default language and the respective language server will be used as a fallback.
|
||||||
|
# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored.
|
||||||
|
languages:
|
||||||
|
- rust
|
||||||
|
|
||||||
|
# the encoding used by text files in the project
|
||||||
|
# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings
|
||||||
|
encoding: "utf-8"
|
||||||
|
|
||||||
|
# whether to use project's .gitignore files to ignore files
|
||||||
|
ignore_all_files_in_gitignore: true
|
||||||
|
|
||||||
|
# list of additional paths to ignore in all projects
|
||||||
|
# same syntax as gitignore, so you can use * and **
|
||||||
|
ignored_paths: []
|
||||||
|
|
||||||
|
# whether the project is in read-only mode
|
||||||
|
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
|
||||||
|
# Added on 2025-04-18
|
||||||
|
read_only: false
|
||||||
|
|
||||||
|
# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
|
||||||
|
# Below is the complete list of tools for convenience.
|
||||||
|
# To make sure you have the latest list of tools, and to view their descriptions,
|
||||||
|
# execute `uv run scripts/print_tool_overview.py`.
|
||||||
|
#
|
||||||
|
# * `activate_project`: Activates a project by name.
|
||||||
|
# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
|
||||||
|
# * `create_text_file`: Creates/overwrites a file in the project directory.
|
||||||
|
# * `delete_lines`: Deletes a range of lines within a file.
|
||||||
|
# * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
|
||||||
|
# * `execute_shell_command`: Executes a shell command.
|
||||||
|
# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
|
||||||
|
# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
|
||||||
|
# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
|
||||||
|
# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
|
||||||
|
# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
|
||||||
|
# * `initial_instructions`: Gets the initial instructions for the current project.
|
||||||
|
# Should only be used in settings where the system prompt cannot be set,
|
||||||
|
# e.g. in clients you have no control over, like Claude Desktop.
|
||||||
|
# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
|
||||||
|
# * `insert_at_line`: Inserts content at a given line in a file.
|
||||||
|
# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
|
||||||
|
# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
|
||||||
|
# * `list_memories`: Lists memories in Serena's project-specific memory store.
|
||||||
|
# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
|
||||||
|
# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
|
||||||
|
# * `read_file`: Reads a file within the project directory.
|
||||||
|
# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
|
||||||
|
# * `remove_project`: Removes a project from the Serena configuration.
|
||||||
|
# * `replace_lines`: Replaces a range of lines within a file with new content.
|
||||||
|
# * `replace_symbol_body`: Replaces the full definition of a symbol.
|
||||||
|
# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
|
||||||
|
# * `search_for_pattern`: Performs a search for a pattern in the project.
|
||||||
|
# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
|
||||||
|
# * `switch_modes`: Activates modes by providing a list of their names
|
||||||
|
# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
|
||||||
|
# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
|
||||||
|
# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
|
||||||
|
# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
|
||||||
|
excluded_tools: []
|
||||||
|
|
||||||
|
# initial prompt for the project. It will always be given to the LLM upon activating the project
|
||||||
|
# (contrary to the memories, which are loaded on demand).
|
||||||
|
initial_prompt: ""
|
||||||
|
|
||||||
|
project_name: "tele-tui"
|
||||||
|
included_optional_tools: []
|
||||||
37
Cargo.lock
generated
37
Cargo.lock
generated
@@ -989,6 +989,25 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "is-docker"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3"
|
||||||
|
dependencies = [
|
||||||
|
"once_cell",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "is-wsl"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5"
|
||||||
|
dependencies = [
|
||||||
|
"is-docker",
|
||||||
|
"once_cell",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itertools"
|
name = "itertools"
|
||||||
version = "0.13.0"
|
version = "0.13.0"
|
||||||
@@ -1175,6 +1194,17 @@ version = "1.21.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "open"
|
||||||
|
version = "5.3.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "43bb73a7fa3799b198970490a51174027ba0d4ec504b03cd08caf513d40024bc"
|
||||||
|
dependencies = [
|
||||||
|
"is-wsl",
|
||||||
|
"libc",
|
||||||
|
"pathdiff",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl"
|
name = "openssl"
|
||||||
version = "0.10.75"
|
version = "0.10.75"
|
||||||
@@ -1254,6 +1284,12 @@ version = "1.0.15"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pathdiff"
|
||||||
|
version = "0.2.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pbkdf2"
|
name = "pbkdf2"
|
||||||
version = "0.12.2"
|
version = "0.12.2"
|
||||||
@@ -1878,6 +1914,7 @@ dependencies = [
|
|||||||
"chrono",
|
"chrono",
|
||||||
"crossterm",
|
"crossterm",
|
||||||
"dotenvy",
|
"dotenvy",
|
||||||
|
"open",
|
||||||
"ratatui",
|
"ratatui",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ serde = { version = "1.0", features = ["derive"] }
|
|||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
dotenvy = "0.15"
|
dotenvy = "0.15"
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
|
open = "5.0"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tdlib-rs = { version = "1.1", features = ["download-tdlib"] }
|
tdlib-rs = { version = "1.1", features = ["download-tdlib"] }
|
||||||
|
|||||||
@@ -100,3 +100,6 @@ cargo run
|
|||||||
5. Ждёт фидбек
|
5. Ждёт фидбек
|
||||||
6. Переходит к следующему этапу
|
6. Переходит к следующему этапу
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Работа с git
|
||||||
|
- никогда не добавляй себя в соавторов в тексте коммита
|
||||||
@@ -66,6 +66,15 @@ pub struct App {
|
|||||||
pub message_search_results: Vec<crate::tdlib::client::MessageInfo>,
|
pub message_search_results: Vec<crate::tdlib::client::MessageInfo>,
|
||||||
/// Индекс выбранного результата
|
/// Индекс выбранного результата
|
||||||
pub selected_search_result_index: usize,
|
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 {
|
impl App {
|
||||||
@@ -106,6 +115,10 @@ impl App {
|
|||||||
message_search_query: String::new(),
|
message_search_query: String::new(),
|
||||||
message_search_results: Vec::new(),
|
message_search_results: Vec::new(),
|
||||||
selected_search_result_index: 0,
|
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();
|
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;
|
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() {
|
if app.is_message_search_mode() {
|
||||||
match key.code {
|
match key.code {
|
||||||
@@ -468,6 +568,29 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
|
|||||||
return;
|
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 {
|
match key.code {
|
||||||
KeyCode::Backspace => {
|
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,
|
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)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub enum NetworkState {
|
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(
|
pub async fn load_older_messages(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
|||||||
@@ -3,4 +3,5 @@ pub mod client;
|
|||||||
pub use client::TdClient;
|
pub use client::TdClient;
|
||||||
pub use client::UserOnlineStatus;
|
pub use client::UserOnlineStatus;
|
||||||
pub use client::NetworkState;
|
pub use client::NetworkState;
|
||||||
|
pub use client::ProfileInfo;
|
||||||
pub use tdlib_rs::enums::ChatAction;
|
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 {
|
fn format_was_online(timestamp: i32) -> String {
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
crate::utils::format_was_online(timestamp)
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
|||||||
} else if app.is_searching {
|
} else if app.is_searching {
|
||||||
format!(" {}↑/↓: Navigate | Enter: Select | Esc: Cancel ", network_indicator)
|
format!(" {}↑/↓: Navigate | Enter: Select | Esc: Cancel ", network_indicator)
|
||||||
} else if app.selected_chat_id.is_some() {
|
} 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 {
|
} 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)
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -310,6 +310,14 @@ fn adjust_entities_for_substring(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
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() {
|
if app.is_message_search_mode() {
|
||||||
render_search_mode(f, area, app);
|
render_search_mode(f, area, app);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ mod main_screen;
|
|||||||
mod chat_list;
|
mod chat_list;
|
||||||
mod messages;
|
mod messages;
|
||||||
mod footer;
|
mod footer;
|
||||||
|
pub mod profile;
|
||||||
|
|
||||||
use ratatui::Frame;
|
use ratatui::Frame;
|
||||||
use ratatui::layout::Alignment;
|
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)
|
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