add pinned messages
This commit is contained in:
@@ -50,6 +50,13 @@ pub struct App {
|
||||
// Typing indicator
|
||||
/// Время последней отправки typing status (для throttling)
|
||||
pub last_typing_sent: Option<std::time::Instant>,
|
||||
// Pinned messages mode
|
||||
/// Режим просмотра закреплённых сообщений
|
||||
pub is_pinned_mode: bool,
|
||||
/// Список закреплённых сообщений
|
||||
pub pinned_messages: Vec<crate::tdlib::client::MessageInfo>,
|
||||
/// Индекс выбранного pinned сообщения
|
||||
pub selected_pinned_index: usize,
|
||||
}
|
||||
|
||||
impl App {
|
||||
@@ -83,6 +90,9 @@ impl App {
|
||||
forwarding_message_id: None,
|
||||
is_selecting_forward_chat: false,
|
||||
last_typing_sent: None,
|
||||
is_pinned_mode: false,
|
||||
pinned_messages: Vec::new(),
|
||||
selected_pinned_index: 0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,10 +150,15 @@ impl App {
|
||||
self.selected_message_index = None;
|
||||
self.replying_to_message_id = None;
|
||||
self.last_typing_sent = None;
|
||||
// Сбрасываем pinned режим
|
||||
self.is_pinned_mode = false;
|
||||
self.pinned_messages.clear();
|
||||
self.selected_pinned_index = 0;
|
||||
// Очищаем данные в TdClient
|
||||
self.td_client.current_chat_id = None;
|
||||
self.td_client.current_chat_messages.clear();
|
||||
self.td_client.typing_status = None;
|
||||
self.td_client.current_pinned_message = None;
|
||||
}
|
||||
|
||||
/// Начать выбор сообщения для редактирования (при стрелке вверх в пустом инпуте)
|
||||
@@ -382,4 +397,51 @@ impl App {
|
||||
self.td_client.current_chat_messages.iter().find(|m| m.id == id)
|
||||
})
|
||||
}
|
||||
|
||||
// === Pinned messages mode ===
|
||||
|
||||
/// Проверка режима pinned
|
||||
pub fn is_pinned_mode(&self) -> bool {
|
||||
self.is_pinned_mode
|
||||
}
|
||||
|
||||
/// Войти в режим pinned (вызывается после загрузки pinned сообщений)
|
||||
pub fn enter_pinned_mode(&mut self, messages: Vec<crate::tdlib::client::MessageInfo>) {
|
||||
if !messages.is_empty() {
|
||||
self.pinned_messages = messages;
|
||||
self.selected_pinned_index = 0;
|
||||
self.is_pinned_mode = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Выйти из режима pinned
|
||||
pub fn exit_pinned_mode(&mut self) {
|
||||
self.is_pinned_mode = false;
|
||||
self.pinned_messages.clear();
|
||||
self.selected_pinned_index = 0;
|
||||
}
|
||||
|
||||
/// Выбрать предыдущий pinned (вверх = более старый)
|
||||
pub fn select_previous_pinned(&mut self) {
|
||||
if !self.pinned_messages.is_empty() && self.selected_pinned_index < self.pinned_messages.len() - 1 {
|
||||
self.selected_pinned_index += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Выбрать следующий pinned (вниз = более новый)
|
||||
pub fn select_next_pinned(&mut self) {
|
||||
if self.selected_pinned_index > 0 {
|
||||
self.selected_pinned_index -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Получить текущее выбранное pinned сообщение
|
||||
pub fn get_selected_pinned(&self) -> Option<&crate::tdlib::client::MessageInfo> {
|
||||
self.pinned_messages.get(self.selected_pinned_index)
|
||||
}
|
||||
|
||||
/// Получить ID текущего pinned для перехода в историю
|
||||
pub fn get_selected_pinned_id(&self) -> Option<i64> {
|
||||
self.get_selected_pinned().map(|m| m.id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,9 +22,69 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
|
||||
}
|
||||
return;
|
||||
}
|
||||
KeyCode::Char('p') if has_ctrl => {
|
||||
// Ctrl+P - режим просмотра закреплённых сообщений
|
||||
if app.selected_chat_id.is_some() && !app.is_pinned_mode() {
|
||||
if let Some(chat_id) = app.get_selected_chat_id() {
|
||||
app.status_message = Some("Загрузка закреплённых...".to_string());
|
||||
match timeout(Duration::from_secs(5), app.td_client.get_pinned_messages(chat_id)).await {
|
||||
Ok(Ok(messages)) => {
|
||||
if messages.is_empty() {
|
||||
app.status_message = Some("Нет закреплённых сообщений".to_string());
|
||||
} else {
|
||||
app.enter_pinned_mode(messages);
|
||||
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;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Режим просмотра закреплённых сообщений
|
||||
if app.is_pinned_mode() {
|
||||
match key.code {
|
||||
KeyCode::Esc => {
|
||||
app.exit_pinned_mode();
|
||||
}
|
||||
KeyCode::Up => {
|
||||
app.select_previous_pinned();
|
||||
}
|
||||
KeyCode::Down => {
|
||||
app.select_next_pinned();
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
// Перейти к сообщению в истории
|
||||
if let Some(msg_id) = app.get_selected_pinned_id() {
|
||||
// Ищем индекс сообщения в текущей истории
|
||||
let msg_index = app.td_client.current_chat_messages
|
||||
.iter()
|
||||
.position(|m| m.id == msg_id);
|
||||
|
||||
if let Some(idx) = msg_index {
|
||||
// Вычисляем scroll offset чтобы показать сообщение
|
||||
let total = app.td_client.current_chat_messages.len();
|
||||
app.message_scroll_offset = total.saturating_sub(idx + 5);
|
||||
}
|
||||
app.exit_pinned_mode();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Модалка подтверждения удаления
|
||||
if app.is_confirm_delete_shown() {
|
||||
match key.code {
|
||||
@@ -129,6 +189,8 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
|
||||
Ok(Ok(_)) => {
|
||||
// Загружаем недостающие reply info
|
||||
let _ = timeout(Duration::from_secs(5), app.td_client.fetch_missing_reply_info()).await;
|
||||
// Загружаем последнее закреплённое сообщение
|
||||
let _ = timeout(Duration::from_secs(2), app.td_client.load_current_pinned_message(chat_id)).await;
|
||||
app.status_message = None;
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
@@ -256,6 +318,8 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
|
||||
Ok(Ok(_)) => {
|
||||
// Загружаем недостающие reply info
|
||||
let _ = timeout(Duration::from_secs(5), app.td_client.fetch_missing_reply_info()).await;
|
||||
// Загружаем последнее закреплённое сообщение
|
||||
let _ = timeout(Duration::from_secs(2), app.td_client.load_current_pinned_message(chat_id)).await;
|
||||
app.status_message = None;
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::env;
|
||||
use std::collections::HashMap;
|
||||
use std::time::Instant;
|
||||
use tdlib_rs::enums::{AuthorizationState, ChatAction, ChatList, ChatType, ConnectionState, MessageContent, MessageSender, Update, User, UserStatus};
|
||||
use tdlib_rs::enums::{AuthorizationState, ChatAction, ChatList, ChatType, ConnectionState, MessageContent, MessageSender, SearchMessagesFilter, Update, User, UserStatus};
|
||||
use tdlib_rs::types::TextEntity;
|
||||
|
||||
/// Максимальный размер кэшей пользователей
|
||||
@@ -226,6 +226,8 @@ pub struct TdClient {
|
||||
pub network_state: NetworkState,
|
||||
/// Typing status для текущего чата: (user_id, action_text, timestamp)
|
||||
pub typing_status: Option<(i64, String, Instant)>,
|
||||
/// Последнее закреплённое сообщение текущего чата
|
||||
pub current_pinned_message: Option<MessageInfo>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
@@ -257,6 +259,7 @@ impl TdClient {
|
||||
user_statuses: LruCache::new(MAX_USER_CACHE_SIZE),
|
||||
network_state: NetworkState::Connecting,
|
||||
typing_status: None,
|
||||
current_pinned_message: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1103,6 +1106,65 @@ impl TdClient {
|
||||
Ok(all_messages)
|
||||
}
|
||||
|
||||
/// Загрузка закреплённых сообщений чата
|
||||
pub async fn get_pinned_messages(&mut self, chat_id: i64) -> Result<Vec<MessageInfo>, String> {
|
||||
let result = functions::search_chat_messages(
|
||||
chat_id,
|
||||
"".to_string(), // query
|
||||
None, // sender_id
|
||||
0, // from_message_id
|
||||
0, // offset
|
||||
100, // limit
|
||||
Some(SearchMessagesFilter::Pinned), // filter
|
||||
0, // message_thread_id
|
||||
0, // saved_messages_topic_id
|
||||
self.client_id,
|
||||
)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(tdlib_rs::enums::FoundChatMessages::FoundChatMessages(found)) => {
|
||||
let mut messages: Vec<MessageInfo> = Vec::new();
|
||||
for m in found.messages {
|
||||
messages.push(self.convert_message(&m, chat_id));
|
||||
}
|
||||
// Сообщения приходят от новых к старым, оставляем как есть
|
||||
Ok(messages)
|
||||
}
|
||||
Err(e) => Err(format!("Ошибка загрузки закреплённых: {:?}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Загружает последнее закреплённое сообщение для текущего чата
|
||||
pub async fn load_current_pinned_message(&mut self, chat_id: i64) {
|
||||
let result = functions::search_chat_messages(
|
||||
chat_id,
|
||||
"".to_string(),
|
||||
None,
|
||||
0,
|
||||
0,
|
||||
1, // Только одно сообщение
|
||||
Some(SearchMessagesFilter::Pinned),
|
||||
0,
|
||||
0,
|
||||
self.client_id,
|
||||
)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(tdlib_rs::enums::FoundChatMessages::FoundChatMessages(found)) => {
|
||||
if let Some(m) = found.messages.first() {
|
||||
self.current_pinned_message = Some(self.convert_message(m, chat_id));
|
||||
} else {
|
||||
self.current_pinned_message = None;
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
self.current_pinned_message = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Загрузка старых сообщений (для скролла вверх)
|
||||
pub async fn load_older_messages(
|
||||
&mut self,
|
||||
|
||||
@@ -307,6 +307,12 @@ fn adjust_entities_for_substring(
|
||||
}
|
||||
|
||||
pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
||||
// Режим просмотра закреплённых сообщений
|
||||
if app.is_pinned_mode() {
|
||||
render_pinned_mode(f, area, app);
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(chat) = app.get_selected_chat() {
|
||||
// Вычисляем динамическую высоту инпута на основе длины текста
|
||||
let input_width = area.width.saturating_sub(4) as usize; // -2 для рамок, -2 для "> "
|
||||
@@ -319,14 +325,30 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
||||
// Минимум 3 строки (1 контент + 2 рамки), максимум 10
|
||||
let input_height = (input_lines + 2).min(10).max(3);
|
||||
|
||||
let message_chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Length(3), // Chat header
|
||||
Constraint::Min(0), // Messages
|
||||
Constraint::Length(input_height), // Input box (динамическая высота)
|
||||
])
|
||||
.split(area);
|
||||
// Проверяем, есть ли закреплённое сообщение
|
||||
let has_pinned = app.td_client.current_pinned_message.is_some();
|
||||
|
||||
let message_chunks = if has_pinned {
|
||||
Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Length(3), // Chat header
|
||||
Constraint::Length(1), // Pinned bar
|
||||
Constraint::Min(0), // Messages
|
||||
Constraint::Length(input_height), // Input box (динамическая высота)
|
||||
])
|
||||
.split(area)
|
||||
} else {
|
||||
Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Length(3), // Chat header
|
||||
Constraint::Length(0), // Pinned bar (hidden)
|
||||
Constraint::Min(0), // Messages
|
||||
Constraint::Length(input_height), // Input box (динамическая высота)
|
||||
])
|
||||
.split(area)
|
||||
};
|
||||
|
||||
// Chat header с typing status
|
||||
let typing_action = app.td_client.typing_status.as_ref().map(|(_, action, _)| action.clone());
|
||||
@@ -364,8 +386,31 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
||||
.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 pinned_datetime = crate::utils::format_datetime(pinned_msg.date);
|
||||
let pinned_text = format!("📌 {} {}{}", pinned_datetime, pinned_preview, ellipsis);
|
||||
let pinned_hint = "Ctrl+P";
|
||||
|
||||
let pinned_bar_width = message_chunks[1].width as usize;
|
||||
let text_len = pinned_text.chars().count();
|
||||
let hint_len = pinned_hint.chars().count();
|
||||
let padding = pinned_bar_width.saturating_sub(text_len + hint_len + 2);
|
||||
|
||||
let pinned_line = Line::from(vec![
|
||||
Span::styled(pinned_text, Style::default().fg(Color::Magenta)),
|
||||
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)));
|
||||
f.render_widget(pinned_bar, message_chunks[1]);
|
||||
}
|
||||
|
||||
// Ширина области сообщений (без рамок)
|
||||
let content_width = message_chunks[1].width.saturating_sub(2) as usize;
|
||||
let content_width = message_chunks[2].width.saturating_sub(2) as usize;
|
||||
|
||||
// Messages с группировкой по дате и отправителю
|
||||
let mut lines: Vec<Line> = Vec::new();
|
||||
@@ -616,7 +661,7 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
||||
}
|
||||
|
||||
// Вычисляем скролл с учётом пользовательского offset
|
||||
let visible_height = message_chunks[1].height.saturating_sub(2) as usize;
|
||||
let visible_height = message_chunks[2].height.saturating_sub(2) as usize;
|
||||
let total_lines = lines.len();
|
||||
|
||||
// Базовый скролл (показываем последние сообщения)
|
||||
@@ -650,7 +695,7 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
||||
let messages_widget = Paragraph::new(lines)
|
||||
.block(Block::default().borders(Borders::ALL))
|
||||
.scroll((scroll_offset, 0));
|
||||
f.render_widget(messages_widget, message_chunks[1]);
|
||||
f.render_widget(messages_widget, message_chunks[2]);
|
||||
|
||||
// Input box с wrap для длинного текста и блочным курсором
|
||||
let (input_line, input_title) = if app.is_forwarding() {
|
||||
@@ -752,7 +797,7 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
||||
let input = Paragraph::new(input_line)
|
||||
.block(input_block)
|
||||
.wrap(ratatui::widgets::Wrap { trim: false });
|
||||
f.render_widget(input, message_chunks[2]);
|
||||
f.render_widget(input, message_chunks[3]);
|
||||
} else {
|
||||
let empty = Paragraph::new("Выберите чат")
|
||||
.block(Block::default().borders(Borders::ALL))
|
||||
@@ -767,6 +812,126 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Рендерит режим просмотра закреплённых сообщений
|
||||
fn render_pinned_mode(f: &mut Frame, area: Rect, app: &App) {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Length(3), // Header
|
||||
Constraint::Min(0), // Pinned messages list
|
||||
Constraint::Length(3), // Help bar
|
||||
])
|
||||
.split(area);
|
||||
|
||||
// Header
|
||||
let total = app.pinned_messages.len();
|
||||
let current = app.selected_pinned_index + 1;
|
||||
let header_text = format!("📌 ЗАКРЕПЛЁННЫЕ ({}/{})", current, total);
|
||||
let header = Paragraph::new(header_text)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(Color::Magenta))
|
||||
)
|
||||
.style(Style::default().fg(Color::Magenta).add_modifier(Modifier::BOLD));
|
||||
f.render_widget(header, chunks[0]);
|
||||
|
||||
// Pinned messages list
|
||||
let content_width = chunks[1].width.saturating_sub(2) as usize;
|
||||
let mut lines: Vec<Line> = Vec::new();
|
||||
|
||||
for (idx, msg) in app.pinned_messages.iter().enumerate() {
|
||||
let is_selected = idx == app.selected_pinned_index;
|
||||
|
||||
// Пустая строка между сообщениями
|
||||
if idx > 0 {
|
||||
lines.push(Line::from(""));
|
||||
}
|
||||
|
||||
// Маркер выбора и имя отправителя
|
||||
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() };
|
||||
|
||||
lines.push(Line::from(vec![
|
||||
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),
|
||||
),
|
||||
Span::styled(
|
||||
format!("({})", crate::utils::format_datetime(msg.date)),
|
||||
Style::default().fg(Color::Gray),
|
||||
),
|
||||
]));
|
||||
|
||||
// Текст сообщения (с переносом)
|
||||
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 строки на сообщение
|
||||
lines.push(Line::from(vec![
|
||||
Span::raw(" "), // Отступ
|
||||
Span::styled(wrapped_line.text, Style::default().fg(msg_color)),
|
||||
]));
|
||||
}
|
||||
if wrapped_count > 3 {
|
||||
lines.push(Line::from(vec![
|
||||
Span::raw(" "),
|
||||
Span::styled("...", Style::default().fg(Color::Gray)),
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
if lines.is_empty() {
|
||||
lines.push(Line::from(Span::styled(
|
||||
"Нет закреплённых сообщений",
|
||||
Style::default().fg(Color::Gray),
|
||||
)));
|
||||
}
|
||||
|
||||
// Скролл к выбранному сообщению
|
||||
let visible_height = chunks[1].height.saturating_sub(2) as usize;
|
||||
let lines_per_msg = 5; // Примерно строк на сообщение
|
||||
let selected_line = app.selected_pinned_index * lines_per_msg;
|
||||
let scroll_offset = if selected_line > visible_height / 2 {
|
||||
(selected_line - visible_height / 2) as u16
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let messages_widget = Paragraph::new(lines)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.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::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::Magenta))
|
||||
)
|
||||
.alignment(Alignment::Center);
|
||||
f.render_widget(help, chunks[2]);
|
||||
}
|
||||
|
||||
/// Рендерит модалку подтверждения удаления
|
||||
fn render_delete_confirm_modal(f: &mut Frame, area: Rect) {
|
||||
use ratatui::widgets::Clear;
|
||||
|
||||
29
src/utils.rs
29
src/utils.rs
@@ -79,3 +79,32 @@ pub fn format_date(timestamp: i32) -> String {
|
||||
pub fn get_day(timestamp: i32) -> i64 {
|
||||
timestamp as i64 / 86400
|
||||
}
|
||||
|
||||
/// Форматирование timestamp в полную дату и время (DD.MM.YYYY HH:MM)
|
||||
pub fn format_datetime(timestamp: i32) -> String {
|
||||
let secs = timestamp as i64;
|
||||
|
||||
// Время
|
||||
let hours = ((secs % 86400) / 3600) as u32;
|
||||
let minutes = ((secs % 3600) / 60) as u32;
|
||||
let local_hours = (hours + 3) % 24; // MSK
|
||||
|
||||
// Дата
|
||||
let days_since_epoch = secs / 86400;
|
||||
let year = 1970 + (days_since_epoch / 365) as i32;
|
||||
let day_of_year = days_since_epoch % 365;
|
||||
|
||||
let months = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
||||
let mut month = 1;
|
||||
let mut day = day_of_year as i32;
|
||||
|
||||
for (i, &m) in months.iter().enumerate() {
|
||||
if day < m {
|
||||
month = i + 1;
|
||||
break;
|
||||
}
|
||||
day -= m;
|
||||
}
|
||||
|
||||
format!("{:02}.{:02}.{} {:02}:{:02}", day + 1, month, year, local_hours, minutes)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user