| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384 |
- // TODO: CLI args for save location and server address
- mod actions;
- mod message;
- mod persistence;
- mod ui;
- use cursive::views::{
- Button, Dialog, DummyView, EditView, LinearLayout, NamedView, Panel, ScrollView, SelectView,
- TextArea, TextView,
- };
- use cursive::{event, Cursive};
- use cursive::{traits::*, CursiveExt as _};
- use message::Message;
- use std::collections::HashMap;
- use std::io::{Read as _, Write as _};
- use std::thread;
- use utils::serialize::Serialize;
- use ui::{
- DIALOGUE_MIN_SIZE, INPUT_BUTTON_ID, INPUT_CLEAR_BUTTON_ID, INPUT_FIELD_ID, INPUT_PANEL_ID,
- LANGUAGE_BUTTON_ID, MESSAGE_PANEL_ID, REFRESH_BUTTON_ID, USERNAME_BUTTON_ID, USERNAME_FIELD_ID,
- USERS_PANEL_ID, USER_PANEL_SIZE,
- };
- const MAX_MESSAGE_LENGTH: usize = 512;
- const MAX_USERNAME_LENGTH: usize = 16;
- const DEFAULT_USERNAME_PREFIX: &str = "Myst";
- const SAVE_FILE: &str = "savedata.bin";
- const REMOTE_REFRESH_RATE: u64 = 10;
- // TODO: Add server refresh rate
- #[derive(Clone)]
- pub struct Appdata {
- pub username: String,
- pub language: ui::Language,
- pub blocked_phrases: Vec<String>,
- pub blocked_phrases_censor_char: char,
- pub messages: HashMap<String, Message>,
- pub quick_close_window_count: usize,
- pub api_endpoint: String,
- }
- impl Default for Appdata {
- fn default() -> Self {
- Self::new()
- }
- }
- impl Appdata {
- pub fn new() -> Self {
- Self {
- messages: HashMap::new(),
- username: format!(
- "{}#{}",
- DEFAULT_USERNAME_PREFIX,
- utils::rng::random_numeric_string(4)
- ),
- language: ui::Language::English,
- quick_close_window_count: 0,
- api_endpoint: "http://127.0.0.1:8080".to_string(),
- blocked_phrases: Vec::new(),
- blocked_phrases_censor_char: '*',
- }
- }
- }
- pub fn get_appdata(siv: &mut Cursive) -> Appdata {
- siv.with_user_data(|appdata: &mut Appdata| appdata.clone())
- .expect("Failed to retrieve appdata.")
- }
- pub fn save_appdata(siv: &mut Cursive) {
- let appdata = get_appdata(siv);
- let savedata = persistence::Savedata::from(appdata);
- let bytes = savedata.serialize_checked();
- let mut file = std::fs::File::create(SAVE_FILE).expect("Failed to create savedata file.");
- file.write_all(&bytes)
- .expect("Failed to write savedata file.");
- }
- pub fn load_appdata(siv: &mut Cursive) -> std::io::Result<()> {
- let mut file = std::fs::File::open(SAVE_FILE)?;
- let mut bytes = Vec::new();
- file.read_to_end(&mut bytes)?;
- let savedata = persistence::Savedata::deserialize_checked(bytes).unwrap();
- let appdata = Appdata::from(savedata);
- siv.set_user_data(appdata);
- Ok(())
- }
- fn main() {
- utils::rng::shuffle_rng();
- let mut siv = Cursive::default();
- siv.set_user_data(Appdata::new());
- load_appdata(&mut siv);
- // Global hotkeys
- siv.add_global_callback(event::Key::Backspace, |siv| {
- let _ = siv.focus_name(INPUT_FIELD_ID);
- });
- // Background thread
- {
- let cb_sink = siv.cb_sink().clone();
- let mut timer = 0;
- thread::spawn(move || loop {
- cb_sink
- .send(Box::new(move |siv| {
- ui::visual_update(siv);
- if timer % REMOTE_REFRESH_RATE == 0 {
- actions::load_messages(siv);
- }
- save_appdata(siv);
- }))
- .expect("Failed to send callback from background thread.");
- timer += 1;
- thread::sleep(std::time::Duration::from_secs(1));
- });
- }
- // Users
- let users_panel = {
- let mut users_view: SelectView<String> = SelectView::new();
- users_view.set_on_submit(move |siv, user: &String| actions::on_user_click(siv, user));
- Panel::new(ScrollView::new(users_view)).with_name(USERS_PANEL_ID)
- };
- // Messages
- let messages_panel = {
- let mut view: SelectView<String> = SelectView::new();
- view.set_on_submit(move |siv, message_id: &str| actions::on_message_click(siv, message_id));
- Panel::new(ScrollView::new(view)).with_name(MESSAGE_PANEL_ID)
- };
- // Input
- let input_panel = {
- let input_field = TextArea::new()
- .with_name(INPUT_FIELD_ID)
- .full_width()
- .max_height(10); // NOTE: 10 seems to be the size of 2 vertical buttons
- let input_button =
- Button::new("", move |siv| actions::on_input_submit(siv, INPUT_FIELD_ID))
- .with_name(INPUT_BUTTON_ID);
- let input_clear_button =
- Button::new("", actions::clear_input_field).with_name(INPUT_CLEAR_BUTTON_ID);
- Panel::new(
- LinearLayout::horizontal().child(input_field).child(
- LinearLayout::vertical()
- .child(input_button)
- .child(input_clear_button),
- ),
- )
- .with_name(INPUT_PANEL_ID)
- };
- // Language selector
- let language_button = Button::new("", move |siv| {
- // NOTE: This must match the order in the SelectView
- let current_language_index = match get_appdata(siv).language {
- ui::Language::English => 0,
- ui::Language::Dutch => 1,
- ui::Language::Japanese => 2,
- };
- let language = get_appdata(siv).language;
- ui::keybind_close_once(siv);
- siv.add_layer(
- cursive::views::Dialog::new()
- .content(
- LinearLayout::vertical().child(
- SelectView::<ui::Language>::new()
- .item("English", ui::Language::English)
- .item("Nederlands", ui::Language::Dutch)
- .item("日本語", ui::Language::Japanese)
- .selected(current_language_index)
- .on_submit(move |siv, language| {
- ui::change_language(siv, *language);
- ui::keybind_close_manual_end(siv, false);
- }),
- ),
- )
- .title(ui::Labels::SetLanguage.localize(language))
- .button(ui::Labels::Cancel.localize(language), |siv| {
- ui::keybind_close_manual_end(siv, false);
- })
- .min_size(DIALOGUE_MIN_SIZE),
- );
- })
- .with_name(LANGUAGE_BUTTON_ID);
- // Current username panel
- let current_username_panel = {
- let current_username = get_appdata(&mut siv).username.clone();
- let view = TextView::new(current_username);
- Panel::new(view).with_name(ui::CURRENT_USERNAME_PANEL_ID)
- };
- // Set username
- let username_button = Button::new("", move |siv| {
- let current_name = get_appdata(siv).username.clone();
- let language = get_appdata(siv).language;
- ui::keybind_close_once(siv);
- siv.add_layer(
- cursive::views::Dialog::new()
- .title(ui::Labels::Username.localize(language))
- .content(
- LinearLayout::vertical().child(
- EditView::new()
- .content(current_name)
- .with_name(USERNAME_FIELD_ID)
- .fixed_width(DIALOGUE_MIN_SIZE.0.into()),
- ),
- )
- .button(ui::Labels::Ok.localize(language), |siv| {
- let input = siv
- .call_on_name(USERNAME_FIELD_ID, |view: &mut EditView| view.get_content())
- .expect("Failed to retrieve username input.");
- if message::is_valid_username(&input) {
- // Valid
- siv.with_user_data(|appdata: &mut Appdata| {
- appdata.username = input.to_string();
- });
- ui::visual_update(siv);
- ui::keybind_close_manual_end(siv, false);
- } else {
- // Invalid
- let language = get_appdata(siv).language;
- ui::keybind_close_manual_end(siv, false); // NOTE: Error dialogue overwrites this one, so end it here beforehand.
- ui::error(
- siv,
- ui::Labels::InvalidUsernameExplination.localize(language),
- );
- }
- })
- .button(ui::Labels::Cancel.localize(language), |siv| {
- ui::keybind_close_manual_end(siv, false);
- })
- .min_size(DIALOGUE_MIN_SIZE),
- );
- })
- .with_name(USERNAME_BUTTON_ID);
- // Refresh button
- let refresh_button = Button::new("", move |siv| {
- if actions::load_messages(siv).is_err() {
- let language = get_appdata(siv).language;
- ui::error(siv, ui::Labels::FailedToLoadMessages.localize(language));
- }
- ui::visual_update(siv);
- })
- .with_name(REFRESH_BUTTON_ID);
- // Blocked words list
- let blocked_words_list_button = {
- Button::new("", move |siv| {
- let appdata = get_appdata(siv);
- let language = appdata.language;
- let wordslist_id = utils::new_id();
- let wordslist_id_clone = wordslist_id.clone();
- let wordslist_id_typingarea_clone = wordslist_id.clone();
- let mut wordslist_view =
- SelectView::<String>::new().on_submit(move |siv, word: &str| {
- siv.with_user_data(|appdata: &mut Appdata| {
- appdata.blocked_phrases.retain(|x| x != word);
- });
- siv.call_on_name(&wordslist_id_clone, |view: &mut NamedView<SelectView>| {
- let idx = view
- .get_mut()
- .selected_id()
- .expect("Selection should exist when fired from submit.");
- view.get_mut().remove_item(idx);
- });
- ui::visual_update(siv);
- });
- wordslist_view.add_all_str(&appdata.blocked_phrases);
- let typingarea_id = utils::new_id();
- let appdata_typingarea_clone = appdata.clone();
- let typing_area = EditView::new().with_name(&typingarea_id);
- let typing_button = Button::new(ui::Labels::Submit.localize(language), move |siv| {
- let text = siv
- .call_on_name(&typingarea_id, |view: &mut EditView| {
- let s = view.get_content();
- view.set_content("");
- s
- })
- .map(|x| x.to_string())
- .unwrap_or("".to_string());
- if appdata_typingarea_clone.blocked_phrases.contains(&text) || text.is_empty() {
- return;
- }
- siv.with_user_data(|appdata: &mut Appdata| {
- appdata.blocked_phrases.push(text.clone());
- });
- siv.call_on_name(
- &wordslist_id_typingarea_clone,
- |view: &mut NamedView<SelectView>| {
- view.get_mut().add_item_str(text);
- },
- );
- ui::visual_update(siv);
- });
- let d = Dialog::new()
- .title(ui::Labels::BlockedWords.localize(language))
- .button(ui::Labels::Close.localize(language), |siv| {
- ui::keybind_close_manual_end(siv, false);
- })
- .content(
- LinearLayout::vertical()
- .child(ScrollView::new(wordslist_view.with_name(wordslist_id)))
- .child(DummyView.fixed_height(1))
- .child(
- LinearLayout::horizontal()
- .child(typing_area.min_width(DIALOGUE_MIN_SIZE.0 as usize))
- .child(typing_button),
- ),
- );
- ui::keybind_close_once(siv);
- siv.add_layer(d);
- })
- .with_name(ui::BLOCKED_WORDS_BUTTON_ID)
- };
- // Main layout
- let main_layout = LinearLayout::vertical()
- .child(
- LinearLayout::horizontal()
- .child(
- LinearLayout::vertical()
- .child(users_panel.resized(
- cursive::view::SizeConstraint::Fixed(USER_PANEL_SIZE),
- cursive::view::SizeConstraint::Full,
- ))
- .child(current_username_panel),
- )
- .child(messages_panel.full_width()),
- )
- .child(input_panel)
- .child(
- LinearLayout::horizontal()
- .child(language_button)
- .child(DummyView.full_width())
- .child(username_button)
- .child(DummyView.full_width())
- .child(blocked_words_list_button)
- .child(DummyView.full_width())
- .child(refresh_button)
- .full_width(),
- );
- siv.add_fullscreen_layer(main_layout);
- siv.run();
- }
|