| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532 |
- 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<S>(siv: &mut Cursive, title: S, text: S)
- where
- S: Into<String>,
- {
- 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<S>(siv: &mut Cursive, text: S)
- where
- S: Into<String>,
- {
- 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<message::MessageSanitized> = appdata
- .messages
- .clone()
- .values()
- .cloned()
- .map(MessageSanitized::from)
- .collect::<Vec<_>>();
- 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<ScrollView<SelectView>>| {
- 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<ScrollView<SelectView>>| {
- let view = panel.get_inner_mut().get_inner_mut();
- let mut senders: Vec<String> = 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<ScrollView<SelectView>>| {
- 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<Panel<TextView>>| {
- 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<Panel<TextView>>| {
- 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<ScrollView<SelectView>>| {
- panel.set_title(label);
- });
- }
- siv.call_on_name(CHANNEL_NEW_BUTTON_ID, |view: &mut NamedView<Button>| {
- view.get_mut()
- .set_label(Labels::NewChannel.localize(language));
- });
- siv.call_on_name(INPUT_PANEL_ID, |panel: &mut Panel<LinearLayout>| {
- panel.set_title(Labels::TypeAMessage.localize(language));
- });
- siv.call_on_name(INPUT_BUTTON_ID, |view: &mut NamedView<Button>| {
- view.get_mut().set_label(Labels::Send.localize(language));
- });
- siv.call_on_name(INPUT_CLEAR_BUTTON_ID, |view: &mut NamedView<Button>| {
- view.get_mut().set_label(Labels::Clear.localize(language));
- });
- siv.call_on_name(LANGUAGE_BUTTON_ID, |view: &mut NamedView<Button>| {
- view.get_mut()
- .set_label(Labels::SetLanguage.localize(language));
- });
- siv.call_on_name(USERNAME_BUTTON_ID, |view: &mut NamedView<Button>| {
- view.get_mut()
- .set_label(Labels::SetUsername.localize(language));
- });
- siv.call_on_name(REFRESH_BUTTON_ID, |view: &mut NamedView<Button>| {
- view.get_mut()
- .set_label(Labels::RefreshButton.localize(language));
- });
- siv.call_on_name(BLOCKED_WORDS_BUTTON_ID, |view: &mut NamedView<Button>| {
- view.get_mut()
- .set_label(Labels::BlockedWords.localize(language));
- });
- siv.call_on_name(
- CURRENT_USERNAME_PANEL_ID,
- |view: &mut NamedView<Panel<TextView>>| {
- view.get_mut()
- .set_title(Labels::Username.localize(language));
- },
- );
- siv.call_on_name(SERVER_SETTINGS_BUTTON_ID, |view: &mut NamedView<Button>| {
- view.get_mut()
- .set_label(Labels::ServerSettings.localize(language));
- });
- }
- pub fn change_language(siv: &mut Cursive, language: Language) {
- siv.with_user_data(|appdata: &mut Appdata| {
- appdata.language = language;
- })
- .expect("Failed to set language.");
- visual_update(siv);
- }
- pub fn channel_matches(message_channel: &str, reference_channel: &str) -> bool {
- message_channel == reference_channel
- }
|