refactor: replace pictures with libadwaita avatars + add max_avatar_size setting

This commit is contained in:
MeexReay 2025-08-31 22:29:21 +03:00
parent 2ea0414710
commit 544de1df8b
4 changed files with 106 additions and 92 deletions

17
Cargo.lock generated
View File

@ -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",

View File

@ -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 }

View File

@ -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<usize>,
#[arg(long)]
pub max_avatar_size: Option<u64>,
#[arg(long)]
pub oof_update_time: Option<usize>,
#[arg(long)]
pub max_messages: Option<usize>,
@ -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
}

View File

@ -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<RwLock<Vec<libnotify::Notification>>>,
#[cfg(all(not(feature = "libnotify"), not(feature = "notify-rust")))]
notifications: Arc<RwLock<Vec<String>>>,
default_avatar: Pixbuf,
avatars: Arc<Mutex<HashMap<u64, Vec<Picture>>>>,
avatars: Arc<Mutex<HashMap<u64, Vec<Avatar>>>>,
latest_sign: Arc<AtomicU64>
}
@ -264,6 +267,23 @@ fn open_settings(ctx: Arc<Context>, 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<Context>, 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::<usize>() {
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::<usize>() {
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::<usize>() {
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::<usize>() {
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<Context>, 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<Context>, app: &Application) -> UiModel {
notifications: Arc::new(RwLock::new(Vec::<libnotify::Notification>::new())),
#[cfg(all(not(feature = "libnotify"), not(feature = "notify-rust")))]
notifications: Arc::new(RwLock::new(Vec::<String>::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<Context>, 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<Vec<u8>> {
fn load_avatar(url: &str, response_limit: usize) -> Option<Vec<u8>> {
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();