Rain 10 hónapja
szülő
commit
eb884a8a0f
6 módosított fájl, 247 hozzáadás és 16 törlés
  1. 1 1
      client/src/actions.rs
  2. 39 6
      client/src/main.rs
  3. 1 1
      client/src/message.rs
  4. 110 0
      client/src/persistence.rs
  5. 9 6
      client/src/ui.rs
  6. 87 2
      utils/src/serialize.rs

+ 1 - 1
client/src/actions.rs

@@ -3,7 +3,7 @@ use utils::{hash::Hashable as _, serialize::Serialize as _};
 
 use crate::{get_appdata, message::Message, ui, Appdata};
 
-// TODO: Create proper user objects
+// TODO (low): Create proper user objects. Related: add a way to verify a user.
 pub fn on_user_click(siv: &mut Cursive, user: &str) {
     let language = get_appdata(siv).language;
     ui::alert(siv, ui::Labels::User.localize(language), user.into());

+ 39 - 6
client/src/main.rs

@@ -1,8 +1,8 @@
-// TODO: Key/auth system
-// TODO: Args or config for username, language, server, and refresh rate.
+// TODO: CLI args for save location and server address
 
 mod actions;
 mod message;
+mod persistence;
 mod ui;
 
 use cursive::views::{
@@ -13,7 +13,9 @@ 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,
@@ -25,17 +27,20 @@ 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 messages: HashMap<String, Message>,
     pub username: String,
     pub language: ui::Language,
-    pub quick_close_window_count: usize,
-    pub api_endpoint: String,
     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 {
@@ -67,12 +72,39 @@ pub fn get_appdata(siv: &mut Cursive) -> Appdata {
         .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);
@@ -90,6 +122,8 @@ fn main() {
                     if timer % REMOTE_REFRESH_RATE == 0 {
                         actions::load_messages(siv);
                     }
+
+                    save_appdata(siv);
                 }))
                 .expect("Failed to send callback from background thread.");
 
@@ -346,6 +380,5 @@ fn main() {
         );
 
     siv.add_fullscreen_layer(main_layout);
-    ui::change_language(&mut siv, ui::Language::English);
     siv.run();
 }

+ 1 - 1
client/src/message.rs

@@ -115,7 +115,7 @@ impl MessageSanitized {
     }
 }
 
-// TODO: Handle invalid messages better
+// TODO (low): Change how invalid messages are handled.
 impl From<Message> for MessageSanitized {
     fn from(value: Message) -> Self {
         let mut sender = value.sender.clone();

+ 110 - 0
client/src/persistence.rs

@@ -0,0 +1,110 @@
+use utils::serialize::{DeserializationError, Serialize, SetOfSerializedObjects};
+
+use crate::{ui, Appdata};
+
+pub struct Savedata {
+    pub username: String,
+    pub language: ui::Language,
+    pub blocked_phrases: Vec<String>,
+    pub blocked_phrases_censor_char: char,
+}
+
+impl From<Appdata> for Savedata {
+    fn from(value: Appdata) -> Self {
+        Self {
+            username: value.username,
+            language: value.language,
+            blocked_phrases: value.blocked_phrases,
+            blocked_phrases_censor_char: value.blocked_phrases_censor_char,
+        }
+    }
+}
+
+impl From<Savedata> for Appdata {
+    fn from(value: Savedata) -> Self {
+        let mut s = Self::new();
+
+        s.username = value.username;
+        s.language = value.language;
+        s.blocked_phrases = value.blocked_phrases;
+        s.blocked_phrases_censor_char = value.blocked_phrases_censor_char;
+
+        s
+    }
+}
+
+impl Serialize for ui::Language {
+    const SIGNATURE: &'static str = "Language";
+
+    fn serialize_unchecked(&self) -> Vec<u8> {
+        let b = match self {
+            ui::Language::English => 1,
+            ui::Language::Dutch => 2,
+            ui::Language::Japanese => 3,
+        };
+
+        vec![b]
+    }
+
+    fn deserialize_unchecked<B>(data: B) -> utils::serialize::Result<Self>
+    where
+        B: AsRef<[u8]>,
+        Self: Sized,
+    {
+        let data = data.as_ref();
+
+        if data.len() != 1 {
+            return Err(DeserializationError::InvalidLength);
+        }
+        let byte = data[0];
+
+        let language = match byte {
+            1 => ui::Language::English,
+            2 => ui::Language::Dutch,
+            3 => ui::Language::Japanese,
+            _ => return Err(DeserializationError::InvalidData("Invalid language byte")),
+        };
+
+        Ok(language)
+    }
+}
+
+impl Serialize for Savedata {
+    const SIGNATURE: &'static str = "Savedata";
+
+    fn serialize_unchecked(&self) -> Vec<u8> {
+        let mut data = Vec::new();
+
+        data.push(self.username.serialize_checked());
+        data.push(self.language.serialize_checked());
+        data.push(self.blocked_phrases.serialize_checked());
+        data.push(self.blocked_phrases_censor_char.serialize_checked());
+
+        data.serialize_checked()
+    }
+
+    fn deserialize_unchecked<B>(data: B) -> utils::serialize::Result<Self>
+    where
+        B: AsRef<[u8]>,
+        Self: Sized,
+    {
+        let data = data.as_ref();
+        let serialized_items = SetOfSerializedObjects::deserialize_checked(data)?;
+
+        if serialized_items.len() != 4 {
+            return Err(DeserializationError::InvalidLength);
+        }
+
+        let username = String::deserialize_checked(&serialized_items[0])?;
+        let language = ui::Language::deserialize_checked(&serialized_items[1])?;
+        let blocked_phrases = Vec::<String>::deserialize_checked(&serialized_items[2])?;
+        let blocked_phrases_censor_char = char::deserialize_checked(&serialized_items[3])?;
+
+        Ok(Self {
+            username,
+            language,
+            blocked_phrases,
+            blocked_phrases_censor_char,
+        })
+    }
+}

+ 9 - 6
client/src/ui.rs

@@ -256,13 +256,9 @@ pub fn visual_update(siv: &mut Cursive) {
             view.get_mut().get_inner_mut().set_content(appdata.username);
         },
     );
-}
 
-pub fn change_language(siv: &mut Cursive, language: Language) {
-    siv.with_user_data(|appdata: &mut Appdata| {
-        appdata.language = language;
-    })
-    .expect("Failed to set language.");
+    // Localize buttons
+    let language = get_appdata(siv).language;
 
     for (name, label) in [
         (USERS_PANEL_ID, Labels::Users.localize(language)),
@@ -312,6 +308,13 @@ pub fn change_language(siv: &mut Cursive, language: Language) {
                 .set_title(Labels::Username.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);
 }

+ 87 - 2
utils/src/serialize.rs

@@ -20,7 +20,7 @@ impl std::fmt::Display for DeserializationError {
 }
 
 pub trait Serialize {
-    const SIGNATURE: &'static str = "";
+    const SIGNATURE: &'static str;
 
     fn serialize_unchecked(&self) -> Vec<u8>;
     fn deserialize_unchecked<B>(data: B) -> Result<Self>
@@ -65,7 +65,7 @@ impl Serialize for SetOfSerializedObjects {
     fn serialize_unchecked(&self) -> Vec<u8> {
         let mut lengths: Vec<SetOfSerializedObjectsLengthMarker> = Vec::new();
         for item in self {
-            // TODO: Account for wrapping
+            // TODO (low): Account for wrapping
             lengths.push(item.len() as SetOfSerializedObjectsLengthMarker);
         }
 
@@ -166,3 +166,88 @@ mod test {
         assert_eq!(data, deserialized);
     }
 }
+
+pub type SetOfSerializableObjects<S> = Vec<S>;
+
+impl<S> Serialize for SetOfSerializableObjects<S>
+where
+    S: Serialize,
+{
+    const SIGNATURE: &'static str = "SetOfSerializableObjects";
+
+    fn serialize_unchecked(&self) -> Vec<u8> {
+        let mut serialized = Vec::new();
+
+        for item in self {
+            serialized.push(item.serialize_checked());
+        }
+
+        serialized.serialize_checked()
+    }
+
+    fn deserialize_unchecked<B>(data: B) -> Result<Self>
+    where
+        B: AsRef<[u8]>,
+        Self: Sized,
+    {
+        let data = data.as_ref();
+        let serialized_items = SetOfSerializedObjects::deserialize_checked(data)?;
+
+        let mut items = Vec::new();
+        for serialized_item in serialized_items {
+            let item = S::deserialize_checked(serialized_item)?;
+            items.push(item);
+        }
+
+        Ok(items)
+    }
+}
+
+impl Serialize for String {
+    const SIGNATURE: &'static str = "String";
+
+    fn serialize_unchecked(&self) -> Vec<u8> {
+        self.as_bytes().to_vec()
+    }
+
+    fn deserialize_unchecked<B>(data: B) -> Result<Self>
+    where
+        B: AsRef<[u8]>,
+        Self: Sized,
+    {
+        let data = data.as_ref();
+
+        let string = String::from_utf8(data.to_vec())
+            .map_err(|_| DeserializationError::InvalidData("Invalid UTF-8"))?;
+
+        Ok(string)
+    }
+}
+
+impl Serialize for char {
+    const SIGNATURE: &'static str = "char";
+
+    fn serialize_unchecked(&self) -> Vec<u8> {
+        (*self as u32).to_be_bytes().to_vec()
+    }
+
+    fn deserialize_unchecked<B>(data: B) -> Result<Self>
+    where
+        B: AsRef<[u8]>,
+        Self: Sized,
+    {
+        let data = data.as_ref();
+
+        if data.len() != 4 {
+            return Err(DeserializationError::InvalidLength);
+        }
+
+        let mut buf = [0u8; 4];
+        buf.copy_from_slice(data);
+
+        let i = u32::from_be_bytes(buf);
+        let c = char::from_u32(i).ok_or(DeserializationError::InvalidData("Not a valid char."))?;
+
+        Ok(c)
+    }
+}