use cursive::view::Resizable as _; use cursive::views::{ Button, Dialog, LinearLayout, NamedView, Panel, ScrollView, SelectView, TextView, }; use cursive::{event, Cursive}; use utils::hash::Hashable as _; use crate::actions::NetworkError; use crate::message::MessageSanitized; use crate::{get_appdata, message, Appdata, MAX_MESSAGE_LENGTH, MAX_USERNAME_LENGTH}; pub const USERS_PANEL_ID: &str = "users_view_id"; pub const MESSAGE_PANEL_ID: &str = "message_view_id"; pub const CHANNEL_VIEW_ID: &str = "channel_view_id"; pub const CHANNEL_NEW_FIELD_ID: &str = "channel_new_field_id"; pub const CHANNEL_NEW_BUTTON_ID: &str = "CHANNEL_NEW_BUTTON_ID"; pub const CHANNEL_CURRENT_PANEL_ID: &str = "CHANNEL_CURRENT_PANEL_ID"; pub const INPUT_FIELD_ID: &str = "input_field_id"; pub const INPUT_PANEL_ID: &str = "input_panel_id"; pub const INPUT_BUTTON_ID: &str = "input_button_id"; pub const INPUT_CLEAR_BUTTON_ID: &str = "input_clear_button_id"; pub const LANGUAGE_BUTTON_ID: &str = "language_button_id"; pub const CURRENT_USERNAME_PANEL_ID: &str = "current_username_view_id"; pub const USERNAME_FIELD_ID: &str = "username_field_id"; pub const USERNAME_BUTTON_ID: &str = "username_button_id"; pub const REFRESH_BUTTON_ID: &str = "refresh_button_id"; pub const BLOCKED_WORDS_BUTTON_ID: &str = "blocked_words_view_id"; pub const SERVER_SETTINGS_ADDRESS_FIELD_ID: &str = "server_settings_address_field_id"; pub const SERVER_SETTINGS_REFRESH_FIELD_ID: &str = "server_settings_refresh_field_id"; pub const SERVER_SETTINGS_PASSWORD_FIELD_ID: &str = "SERVER_SETTINGS_PASSWORD_FIELD_ID"; pub const SERVER_SETTINGS_BUTTON_ID: &str = "server_settings_button_id"; pub const DIALOGUE_MIN_SIZE: (u16, u16) = (20, 5); pub const SERVER_SETTINGS_FIELD_SIZE: (usize, usize) = (60, 1); pub const USER_PANEL_SIZE: usize = crate::MAX_USERNAME_LENGTH + 2; #[allow(unused)] pub enum Labels { Ok, Cancel, Close, Submit, Send, New, Clear, Error, User, Users, Message, Messages, Channel, Channels, NewChannel, InvalidChannelNameExplination, TypeAMessage, SetLanguage, SetUsername, InvalidUsernameExplination, Username, InvalidMessage, FailedToSendMessage(NetworkError), FailedToLoadMessages(NetworkError), RefreshButton, BlockedWords, ServerSettings, ServerAddress, ServerRefreshRate, Password, } #[derive(Debug, Clone, Copy)] pub enum Language { English, Dutch, Japanese, } impl Labels { // TODO: Double check the translations pub fn localize<'a>(&self, language: Language) -> String { let buf: [String; 3]; let set: [&str; 3] = match self { Labels::Ok => ["OK", "OK", "OK"], Labels::Cancel => ["Cancel", "Annuleren", "キャンセル"], Labels::Close => ["Close", "Sluiten", "閉じる"], Labels::Error => ["Error", "Fout", "エラー"], Labels::User => ["User", "Gebruiker", "ユーザー"], Labels::Messages => ["Messages", "Berichten", "メッセージ"], Labels::Message => ["Message", "Bericht", "メッセージ"], Labels::Users => ["Users", "Gebruikers", "ユーザー"], Labels::TypeAMessage => [ "Type a message", "Typ een bericht", "メッセージを入力してください", ], Labels::SetUsername => [ "Set username", "Stel gebruikersnaam in", "ユーザー名を設定する", ], Labels::Send => ["Send", "Verzenden", "送信する"], Labels::Clear => ["Clear", "Wissen", "クリア"], Labels::Username => ["Username", "Gebruikersnaam", "ユーザー名"], Labels::SetLanguage => ["Set language", "Stel taal in", "言語を設定する"], Labels::FailedToSendMessage(e) => { let reason_buf: [String; 3]; match e { NetworkError::ReqwestError(_) => { reason_buf = [ "Connection error.".to_string(), "Verbindingsfout.".to_string(), "接続エラー。".to_string(), ]; } NetworkError::StatusCodeError(code) => { // NOTE: Needs to match the server if *code == 400 { reason_buf = [ "Message too long.".to_string(), "Bericht te lang.".to_string(), "メッセージが長すぎます。".to_string(), ]; } else { reason_buf = [ format!("Status code: {code}"), format!("Statuscode: {code}"), format!("ステータスコード: {code}"), ] } } NetworkError::InvalidUrl => { reason_buf = [ "Invalid URL.".to_string(), "Ongeldig URL.".to_string(), "無効なURL。".to_string(), ]; } NetworkError::EncryptionError(_) => { reason_buf = [ "Encryption error.".to_string(), "Encryptiefout.".to_string(), "暗号化エラー。".to_string(), ]; } }; buf = [ format!("Failed to send message. {}", reason_buf[0]), format!("Bericht verzenden mislukt. {}", reason_buf[1]), format!("メッセージの送信に失敗しました。 {}", reason_buf[2]), ]; [buf[0].as_str(), buf[1].as_str(), buf[2].as_str()] } Labels::FailedToLoadMessages(error) => { let reason_buf: [String; 3]; match error { NetworkError::ReqwestError(_) => { reason_buf = [ "Connection error.".to_string(), "Verbindingsfout.".to_string(), "接続エラー。".to_string(), ]; } NetworkError::StatusCodeError(code) => { reason_buf = [ format!("Status code: {code}"), format!("Statuscode: {code}"), format!("ステータスコード: {code}"), ]; } NetworkError::InvalidUrl => { reason_buf = [ "Invalid URL.".to_string(), "Ongeldig URL.".to_string(), "無効なURL。".to_string(), ]; } NetworkError::EncryptionError(_) => { reason_buf = [ "Decryption error.".to_string(), "Fout bij het ontcijferen.".to_string(), "復号エラー".to_string(), ]; } }; buf = [ format!("Failed to load messages. {}", reason_buf[0]), format!("Berichten laden mislukt. {}", reason_buf[1]), format!("メッセージの読み込みに失敗しました。 {}", reason_buf[2]), ]; [buf[0].as_str(), buf[1].as_str(), buf[2].as_str()] } Labels::RefreshButton => ["Refresh", "Vernieuwen", "更新する"], Labels::InvalidUsernameExplination => { buf = [ r"Invalid username. Must match ^[a-zA-Z0-9#_\-\.]{2,${MAX_USERNAME_LENGTH}}$" .replace("${MAX_USERNAME_LENGTH}", &MAX_USERNAME_LENGTH.to_string()), r"Ongeldige gebruikersnaam. Moet overeenkomen met ^[a-zA-Z0-9#_\-\.]{2,${MAX_USERNAME_LENGTH}}$" .replace("${MAX_USERNAME_LENGTH}", &MAX_USERNAME_LENGTH.to_string()), r"無効なユーザー名。 ^[a-zA-Z0-9#_\-\.]{2,${MAX_USERNAME_LENGTH}}$ に一致する必要があります" .replace("${MAX_USERNAME_LENGTH}", &MAX_USERNAME_LENGTH.to_string()), ]; [buf[0].as_str(), buf[1].as_str(), buf[2].as_str()] } Labels::InvalidMessage => { buf = [ "Invalid message. Must contain fewer than ${MAX_MESSAGE_LENGTH} characters" .replace("${MAX_MESSAGE_LENGTH}", &MAX_MESSAGE_LENGTH.to_string()), "Ongeldig bericht. Moet minder dan ${MAX_MESSAGE_LENGTH} tekens bevatten" .replace("${MAX_MESSAGE_LENGTH}", &MAX_MESSAGE_LENGTH.to_string()), "無効なメッセージ。 ${MAX_MESSAGE_LENGTH}文字未満である必要があります" .replace("${MAX_MESSAGE_LENGTH}", &MAX_MESSAGE_LENGTH.to_string()), ]; [buf[0].as_str(), buf[1].as_str(), buf[2].as_str()] } Labels::BlockedWords => [ "Blocked words", "Geblokkeerde woorden", "ブロックされた単語", ], Labels::Submit => ["Submit", "Indienen", "提出する"], Labels::ServerSettings => ["Server settings", "Serverinstellingen", "サーバー設定"], Labels::ServerAddress => [ "Server address (IP:Port)", "Serveradres (IP:poort)", "サーバーアドレス(IP:ポート)", ], Labels::ServerRefreshRate => [ "Update rate (seconds)", "Updatefrequentie (seconden)", "更新率 (秒)", ], Labels::Channels => ["Channels", "Kanalen", "チャンネル"], Labels::NewChannel => ["New Channel", "Nieuw kanaal", "新しいチャンネル"], Labels::InvalidChannelNameExplination => { buf = [ r"Invalid channel name. Must match ^[a-zA-Z0-9#_\-\.]{2,${MAX_USERNAME_LENGTH}}$" .replace("${MAX_USERNAME_LENGTH}", &MAX_USERNAME_LENGTH.to_string()), r"Ongeldige kanaalnaam. Moet overeenkomen met ^[a-zA-Z0-9#_\-\.]{2,${MAX_USERNAME_LENGTH}}$" .replace("${MAX_USERNAME_LENGTH}", &MAX_USERNAME_LENGTH.to_string()), r"無効なチャンネル名。 ^[a-zA-Z0-9#_\-\.]{2,${MAX_USERNAME_LENGTH}}$ に一致する必要があります" .replace("${MAX_USERNAME_LENGTH}", &MAX_USERNAME_LENGTH.to_string()), ]; [buf[0].as_str(), buf[1].as_str(), buf[2].as_str()] } Labels::New => ["New", "Nieuw", "新"], Labels::Channel => ["Channel", "Kanaal", "チャネル"], Labels::Password => ["Password", "Wachtwoord", "パスワード"], }; let idx = match language { Language::English => 0, Language::Dutch => 1, Language::Japanese => 2, }; set[idx].to_string() } } pub fn alert(siv: &mut Cursive, title: S, text: S) where S: Into, { let language = get_appdata(siv).language; keybind_setup_close_once(siv); siv.add_layer( Dialog::text(text) .title(title) .button(Labels::Close.localize(language), |siv| { keybind_close_manual_end(siv, false); }) .min_size((20, 5)), ); } pub fn error(siv: &mut Cursive, text: S) where S: Into, { let language = get_appdata(siv).language; alert(siv, Labels::Error.localize(language), text.into()); } /// Sets up hotkey to close the most recent view pub fn keybind_setup_close_once(siv: &mut Cursive) { siv.with_user_data(|appdata: &mut Appdata| { appdata.quick_close_window_count += 1; }); siv.add_global_callback(event::Key::Esc, |siv| { keybind_close_manual_end(siv, false); }); } // `close_all` can be removed, it isn't used. /// Manually close the most recent view and removes the hotkey pub fn keybind_close_manual_end(siv: &mut Cursive, close_all: bool) { siv.with_user_data(|appdata: &mut Appdata| { if close_all { appdata.quick_close_window_count = 0; } else { appdata.quick_close_window_count = appdata.quick_close_window_count.saturating_sub(1); } }); if get_appdata(siv).quick_close_window_count == 0 { siv.clear_global_callbacks(event::Key::Esc); } siv.pop_layer(); } pub fn visual_update(siv: &mut Cursive) { let appdata = get_appdata(siv); let current_channel = appdata.current_channel; let language = get_appdata(siv).language; // --- Messages --- let mut messages: Vec = appdata .messages .clone() .values() .cloned() .map(MessageSanitized::from) .collect::>(); messages.sort_by(|a, b| a.time.cmp(&b.time)); // Remove blocked phrases for message in messages.iter_mut() { message.remove_blocked_phrases(siv); } siv.call_on_name( MESSAGE_PANEL_ID, |panel: &mut Panel>| { let view = panel.get_inner_mut().get_inner_mut(); let selected = view.selected_id(); view.clear(); for message in messages .iter() .filter(|x| channel_matches(&x.channel, ¤t_channel)) { view.add_item( // TODO: Localized timestamps format!( "{:>6} | {}: {}\n", utils::time::timestamp_relative(message.time), message.sender, message.content, ), message.hash().to_string(), ); } if let Some(selected) = selected { view.set_selection(selected); } }, ); // --- Members list --- siv.call_on_name( USERS_PANEL_ID, |panel: &mut Panel>| { let view = panel.get_inner_mut().get_inner_mut(); let mut senders: Vec = vec![]; for message in messages .iter() .filter(|x| channel_matches(&x.channel, ¤t_channel)) { if senders.contains(&message.sender) { continue; } senders.push(message.sender.clone()); } senders.sort(); let selected = view.selected_id(); view.clear(); for sender in senders { view.add_item(sender.clone(), sender.clone()); } if let Some(selected) = selected { view.set_selection(selected); } }, ); // --- Channels --- siv.call_on_name( CHANNEL_VIEW_ID, |panel: &mut Panel>| { let view = panel.get_inner_mut().get_inner_mut(); let mut channels = appdata.local_channels.clone(); for message in messages.iter() { if !channels.contains(&message.channel) { channels.push(message.channel.clone()); } } channels.sort(); let selected = view.selected_id(); view.clear(); for channel in channels { view.add_item(channel.clone(), channel.clone()); } if let Some(selected) = selected { view.set_selection(selected); } }, ); // --- Current channel --- let appdata_c = get_appdata(siv); siv.call_on_name( CHANNEL_CURRENT_PANEL_ID, |view: &mut NamedView>| { view.get_mut() .get_inner_mut() .set_content(appdata_c.current_channel); view.get_mut().set_title(Labels::Channel.localize(language)); }, ); // --- Current username --- siv.call_on_name( CURRENT_USERNAME_PANEL_ID, |view: &mut NamedView>| { view.get_mut().get_inner_mut().set_content(appdata.username); }, ); // --- Localize buttons --- for (name, label) in [ (USERS_PANEL_ID, Labels::Users.localize(language)), (MESSAGE_PANEL_ID, Labels::Messages.localize(language)), (CHANNEL_VIEW_ID, Labels::Channels.localize(language)), ] { siv.call_on_name(name, |panel: &mut Panel>| { panel.set_title(label); }); } siv.call_on_name(CHANNEL_NEW_BUTTON_ID, |view: &mut NamedView