From 544de1df8b5e9c1be757cd62c5179804e9f8aa5f Mon Sep 17 00:00:00 2001 From: MeexReay Date: Sun, 31 Aug 2025 22:29:21 +0300 Subject: [PATCH] refactor: replace pictures with libadwaita avatars + add max_avatar_size setting --- Cargo.lock | 17 +++++ Cargo.toml | 3 + src/chat/config.rs | 10 +++ src/chat/gui.rs | 168 ++++++++++++++++++++------------------------- 4 files changed, 106 insertions(+), 92 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ad3ade8..fa1deef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -246,6 +246,7 @@ version = "0.1.6+2.0" dependencies = [ "chrono", "clap", + "futures", "gdk-pixbuf 0.3.0", "homedir", "lazy_static", @@ -711,6 +712,21 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.31" @@ -786,6 +802,7 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ + "futures-channel", "futures-core", "futures-io", "futures-macro", diff --git a/Cargo.toml b/Cargo.toml index 8acf8c4..8e0be3c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,9 @@ winapi = { version = "0.3.9", optional = true, features = ["wincon", "winuser"] tungstenite = "0.27.0" reqwest = { version = "0.12.20", features = ["blocking"] } libadwaita = { version = "0.8.0", optional = true, features = ["v1_5"] } +# glycin = { version = "2.1.1", optional = true } +futures = "0.3.31" +# gio = { version = "0.20.12", optional = true } [build-dependencies] winresource = { version = "0.1.20", optional = true } diff --git a/src/chat/config.rs b/src/chat/config.rs index 0d13fb5..079720f 100644 --- a/src/chat/config.rs +++ b/src/chat/config.rs @@ -11,6 +11,9 @@ fn default_true() -> bool { pub fn default_max_messages() -> usize { 200 } +pub fn default_max_avatar_size() -> u64 { + 5248000 // 5MB +} pub fn default_update_time() -> usize { 100 } @@ -41,6 +44,8 @@ pub struct Config { pub oof_update_time: usize, #[serde(default = "default_max_messages")] pub max_messages: usize, + #[serde(default = "default_max_avatar_size")] + pub max_avatar_size: u64, #[serde(default = "default_konata_size")] pub konata_size: usize, #[serde(default)] @@ -149,6 +154,8 @@ pub struct Args { #[arg(long)] pub update_time: Option, #[arg(long)] + pub max_avatar_size: Option, + #[arg(long)] pub oof_update_time: Option, #[arg(long)] pub max_messages: Option, @@ -204,6 +211,9 @@ impl Args { if let Some(v) = self.konata_size { config.konata_size = v } + if let Some(v) = self.max_avatar_size { + config.max_avatar_size = v + } if let Some(v) = self.hide_my_ip { config.hide_my_ip = v } diff --git a/src/chat/gui.rs b/src/chat/gui.rs index 370c830..7e39e2c 100644 --- a/src/chat/gui.rs +++ b/src/chat/gui.rs @@ -1,10 +1,9 @@ -// TODO: REFACTOR THIS SHIT!!!!!!!!!!!!!!! - use std::cell::RefCell; use std::collections::HashMap; use std::error::Error; use std::hash::{DefaultHasher, Hasher}; -use std::sync::atomic::{AtomicBool, AtomicU64}; +use std::io::Read; +use std::sync::atomic::AtomicU64; use std::sync::Mutex; use std::sync::{atomic::Ordering, mpsc::channel, Arc, RwLock}; use std::thread; @@ -13,25 +12,31 @@ use std::time::{Duration, SystemTime}; use chrono::Local; use clap::crate_version; -use libadwaita::gtk::Adjustment; -use libadwaita::{self as adw, ActionRow, EntryRow, PreferencesDialog, PreferencesGroup, PreferencesPage, PreferencesRow, SpinRow, SwitchRow}; -use adw::gdk::{Cursor, Display, Texture}; -use adw::gio::{self, ActionEntry, ApplicationFlags, MemoryInputStream, Menu}; +use libadwaita::gdk::{MemoryTexture, Texture}; +use libadwaita::gtk::gdk_pixbuf::InterpType; +use libadwaita::gtk::{Adjustment, Image}; +use libadwaita::{ + self as adw, Avatar, EntryRow, + PreferencesDialog, PreferencesGroup, PreferencesPage, + SpinRow, SwitchRow +}; +use adw::gdk::{Cursor, Display}; +use adw::gio::{ActionEntry, ApplicationFlags, MemoryInputStream, Menu}; use adw::glib::clone; use adw::glib::{ - self, clone::Downgrade, source::timeout_add_local_once, timeout_add_local, timeout_add_once, + self, clone::Downgrade, source::timeout_add_local_once, + timeout_add_local, timeout_add_once, ControlFlow, }; use adw::prelude::*; -use adw::{Application, ApplicationWindow, Window}; +use adw::{Application, ApplicationWindow}; use adw::gtk; -use gtk::gdk_pixbuf::InterpType; use gtk::gdk_pixbuf::{Pixbuf, PixbufAnimation, PixbufLoader}; use gtk::pango::WrapMode; use gtk::{ - AboutDialog, Align, Box as GtkBox, Button, Calendar, - CheckButton, CssProvider, Entry, Fixed, GestureClick, Justification, Label, ListBox, + Align, Box as GtkBox, Button, Calendar, + CssProvider, Entry, Fixed, GestureClick, Justification, Label, ListBox, Orientation, Overlay, Picture, ScrolledWindow, Settings, }; @@ -39,7 +44,6 @@ use crate::chat::grab_avatar; use super::{ config::{ - default_konata_size, default_max_messages, default_oof_update_time, default_update_time, get_config_path, save_config, Config, }, ctx::Context, @@ -56,8 +60,7 @@ struct UiModel { notifications: Arc>>, #[cfg(all(not(feature = "libnotify"), not(feature = "notify-rust")))] notifications: Arc>>, - default_avatar: Pixbuf, - avatars: Arc>>>, + avatars: Arc>>>, latest_sign: Arc } @@ -264,6 +267,23 @@ fn open_settings(ctx: Arc, app: &Application) { group.add(&proxy); + + // Max avatar size preference + + let max_avatar_size = SpinRow::builder() + .title("Max avatar size") + .subtitle("Maximum avatar size in bytes") + .adjustment(&Adjustment::builder() + .lower(0.0) + .upper(1074790400.0) + .page_increment(1024.0) + .step_increment(1024.0) + .value(ctx.config(|o| o.max_avatar_size) as f64) + .build()) + .build(); + + group.add(&max_avatar_size); + page.add(&group); @@ -451,50 +471,11 @@ fn open_settings(ctx: Arc, app: &Application) { } }, message_format: message_format.text().to_string(), - update_time: { - let update_time = update_interval.text(); - - if let Ok(update_time) = update_time.parse::() { - update_time - } else { - let update_time = default_update_time(); - update_interval.set_text(&update_time.to_string()); - update_time - } - }, - oof_update_time: { - let oof_update_time = update_interval_oof.text(); - - if let Ok(oof_update_time) = oof_update_time.parse::() { - oof_update_time - } else { - let oof_update_time = default_oof_update_time(); - update_interval_oof.set_text(&oof_update_time.to_string()); - oof_update_time - } - }, - konata_size: { - let konata_size_n = konata_size.text(); - - if let Ok(konata_size_n) = konata_size_n.parse::() { - konata_size_n.max(0).min(200) - } else { - let konata_size_n = default_konata_size(); - konata_size.set_text(&konata_size_n.to_string()); - konata_size_n - } - }, - max_messages: { - let max_messages = messages_limit.text(); - - if let Ok(max_messages) = max_messages.parse::() { - max_messages - } else { - let max_messages = default_max_messages(); - messages_limit.set_text(&max_messages.to_string()); - max_messages - } - }, + update_time: update_interval.value() as usize, + oof_update_time: update_interval_oof.value() as usize, + konata_size: konata_size.value() as usize, + max_messages: messages_limit.value() as usize, + max_avatar_size: max_avatar_size.value() as u64, hide_my_ip: hide_my_ip.is_active(), remove_gui_shit: remove_gui_shit.is_active(), show_other_ip: show_ips.is_active(), @@ -666,7 +647,7 @@ fn build_ui(ctx: Arc, app: &Application) -> UiModel { let logo_anim = PixbufAnimation::from_stream( &MemoryInputStream::from_bytes(&glib::Bytes::from(logo_gif)), - None::<&gio::Cancellable>, + None::<&adw::gtk::gio::Cancellable>, ) .unwrap() .iter(Some(SystemTime::now())); @@ -866,7 +847,6 @@ fn build_ui(ctx: Arc, app: &Application) -> UiModel { notifications: Arc::new(RwLock::new(Vec::::new())), #[cfg(all(not(feature = "libnotify"), not(feature = "notify-rust")))] notifications: Arc::new(RwLock::new(Vec::::new())), - default_avatar: load_pixbuf(include_bytes!("images/avatar.png")).unwrap(), avatars: Arc::new(Mutex::new(HashMap::new())), latest_sign: Arc::new(AtomicU64::new(0)) } @@ -951,38 +931,18 @@ fn setup(_: &Application, ctx: Arc, ui: UiModel) { let Some(avatar_url) = grab_avatar(message) else { continue }; let avatar_id = get_avatar_id(&avatar_url); - let Some(avatar) = load_avatar(&avatar_url) - .and_then(|avatar| load_pixbuf(&avatar).ok()) - .and_then(|pixbuf| - pixbuf.scale_simple(32, 32, InterpType::Bilinear - )) - .and_then(|pixbuf| Some(( - pixbuf.pixel_bytes()?, - pixbuf.colorspace(), - pixbuf.has_alpha(), - pixbuf.bits_per_sample(), - pixbuf.width(), - pixbuf.height(), - pixbuf.rowstride() - ))) else { continue }; + let Some(avatar) = load_avatar(&avatar_url, ctx.config(|o| o.max_avatar_size as usize)) else { println!("cant load avatar: {avatar_url} request error"); continue }; + let Ok(pixbuf) = load_pixbuf(&avatar) else { println!("cant load avatar: {avatar_url} pixbuf error"); continue; }; + let Some(pixbuf) = pixbuf.scale_simple(32, 32, InterpType::Bilinear) else { println!("cant load avatar: {avatar_url} scale image error"); continue }; + let texture = Texture::for_pixbuf(&pixbuf); timeout_add_once(Duration::ZERO, { move || { GLOBAL.with(|global| { if let Some(ui) = &*global.borrow() { - let pixbuf = Pixbuf::from_bytes( - &avatar.0, - avatar.1, - avatar.2, - avatar.3, - avatar.4, - avatar.5, - avatar.6 - ); - if let Some(pics) = ui.avatars.lock().unwrap().remove(&avatar_id) { for pic in pics { - pic.set_pixbuf(Some(&pixbuf)); + pic.set_custom_image(Some(&texture)); } } } @@ -1178,10 +1138,28 @@ fn get_avatar_id(url: &str) -> u64 { hasher.finish() } -fn load_avatar(url: &str) -> Option> { +fn load_avatar(url: &str, response_limit: usize) -> Option> { reqwest::blocking::get(url).ok() - .and_then(|resp| resp.bytes().ok()) - .map(|bytes| bytes.to_vec()) + .and_then(|mut resp| { + let mut data = Vec::new(); + let mut length = 0; + + loop { + if length >= response_limit { + break; + } + let mut buf = vec![0; (response_limit - length).min(1024)]; + let now_len = resp.read(&mut buf).ok()?; + if now_len == 0 { + break; + } + buf.truncate(now_len); + length += now_len; + data.append(&mut buf); + } + + Some(data) + }) } fn get_new_message_box( @@ -1256,13 +1234,19 @@ fn get_new_message_box( let fixed = Fixed::new(); fixed.set_can_target(false); - let avatar_picture = Picture::for_pixbuf(&ui.default_avatar.clone()); - avatar_picture.set_css_classes(&["message-avatar"]); + let avatar_picture = Avatar::builder() + .text(&name) + .show_initials(true) + // .width_request(64) + // .height_request(64) + .size(32) + .build(); + // avatar_picture.set_css_classes(&["message-avatar"]); avatar_picture.set_vexpand(false); avatar_picture.set_hexpand(false); avatar_picture.set_valign(Align::Start); avatar_picture.set_halign(Align::Start); - avatar_picture.set_size_request(32, 32); + // avatar_picture.set_size_request(64, 64); if avatar != 0 { let mut lock = ui.avatars.lock().unwrap();