refactor: add server list to sidebar

This commit is contained in:
MeexReay 2025-09-03 20:14:33 +03:00
parent 02c4862178
commit 431736e967
4 changed files with 369 additions and 199 deletions

View File

@ -3,6 +3,8 @@ use serde_default::DefaultFromSerde;
use serde_yml;
use std::{error::Error, fs, path::PathBuf};
use super::SERVER_LIST;
const MESSAGE_FORMAT: &str = "\u{B9AC}\u{3E70}<{name}> {text}";
fn default_true() -> bool {
@ -30,6 +32,10 @@ pub fn default_message_format() -> String {
MESSAGE_FORMAT.to_string()
}
pub fn default_servers() -> Vec<String> {
SERVER_LIST.to_vec()
}
#[derive(serde::Serialize, serde::Deserialize, DefaultFromSerde, Clone)]
pub struct Config {
#[serde(default = "default_host")]
@ -70,6 +76,8 @@ pub struct Config {
pub debug_logs: bool,
#[serde(default)]
pub avatar: Option<String>,
#[serde(default = "default_servers")]
pub servers: Vec<String>,
}
#[cfg(target_os = "windows")]
@ -183,6 +191,7 @@ pub struct Args {
pub avatar: Option<String>,
#[arg(long)]
pub debug_logs: bool,
// TODO: add servers
}
impl Args {

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -12,40 +12,32 @@ use std::time::{Duration, SystemTime};
use clap::crate_version;
use libadwaita::gdk::Texture;
use libadwaita::gtk::gdk_pixbuf::InterpType;
use libadwaita::gtk::{Button, Label};
use libadwaita::{
self as adw, Avatar, Breakpoint, BreakpointCondition, OverlaySplitView
};
use adw::gdk::Display;
use adw::gio::{ActionEntry, ApplicationFlags, Menu};
use adw::glib::clone;
use adw::glib::{
self, source::timeout_add_local_once, timeout_add_once,
};
use adw::glib::{self, source::timeout_add_local_once, timeout_add_once};
use adw::prelude::*;
use adw::{Application, ApplicationWindow};
use libadwaita::gdk::Texture;
use libadwaita::gtk::gdk_pixbuf::InterpType;
use libadwaita::gtk::{Button, Entry, Label, Picture};
use libadwaita::{self as adw, Avatar, Breakpoint, BreakpointCondition, Dialog, OverlaySplitView};
use adw::gtk;
use gtk::gdk_pixbuf::{Pixbuf, PixbufLoader};
use gtk::{
Box as GtkBox,
CssProvider,
Orientation, ScrolledWindow, Settings,
};
use gtk::{Box as GtkBox, CssProvider, Orientation, ScrolledWindow, Settings};
use crate::chat::grab_avatar;
use super::config::get_config_path;
use super::{
config::{
save_config, Config,
},
ctx::Context, print_message, recv_tick, sanitize_message,
config::{save_config, Config},
ctx::Context,
print_message, recv_tick, sanitize_message,
};
mod preferences;
mod page;
mod preferences;
mod widgets;
use page::*;
@ -53,7 +45,7 @@ use preferences::*;
pub fn try_save_config(path: PathBuf, config: &Config) {
match save_config(path, config) {
Ok(_) => {},
Ok(_) => {}
Err(e) => {
println!("save config error: {e}")
}
@ -71,7 +63,7 @@ struct UiModel {
#[cfg(all(not(feature = "libnotify"), not(feature = "notify-rust")))]
notifications: Arc<RwLock<Vec<String>>>,
avatars: Arc<Mutex<HashMap<u64, Vec<Avatar>>>>,
latest_sign: Arc<AtomicU64>
latest_sign: Arc<AtomicU64>,
}
thread_local!(
@ -163,9 +155,175 @@ fn build_menu(ctx: Arc<Context>, app: &Application) -> Menu {
menu
}
fn build_sidebar(_ctx: Arc<Context>, _app: &Application) -> GtkBox {
let sidebar = GtkBox::new(Orientation::Vertical, 0);
sidebar.append(&Label::new(Some("hello worlding")));
fn build_sidebar_button(
ctx: Arc<Context>,
split_view: &OverlaySplitView,
server: String,
servers_list: &GtkBox,
) -> GtkBox {
let hbox = GtkBox::new(Orientation::Horizontal, 5);
let button = Button::builder().label(&server).hexpand(true).build();
button.connect_clicked(clone!(
#[weak]
split_view,
#[weak]
ctx,
#[strong]
server,
move |_| {
let mut config = ctx.config.read().unwrap().clone();
config.host = server.clone();
ctx.set_config(&config);
try_save_config(get_config_path(), &config);
update_window_title(ctx.clone());
if split_view.is_collapsed() {
split_view.set_show_sidebar(false);
}
}
));
hbox.append(&button);
let delete_button = Button::from_icon_name("user-trash-symbolic");
delete_button.connect_clicked(clone!(
#[weak]
ctx,
#[weak]
hbox,
#[weak]
servers_list,
#[strong]
server,
move |_| {
servers_list.remove(&hbox);
let mut config = ctx.config.read().unwrap().clone();
let index = config.servers.iter().position(|x| *x == server).unwrap();
config.servers.remove(index);
ctx.set_config(&config);
try_save_config(get_config_path(), &config);
}
));
hbox.append(&delete_button);
hbox
}
fn build_sidebar(ctx: Arc<Context>, app: &Application, split_view: &OverlaySplitView) -> GtkBox {
let sidebar = GtkBox::new(Orientation::Vertical, 5);
sidebar.append(
&Picture::builder()
.paintable(&Texture::for_pixbuf(
&load_pixbuf(include_bytes!("images/servers.png")).unwrap(),
))
.build(),
);
let servers_list = GtkBox::new(Orientation::Vertical, 5);
for server in ctx.config(|o| o.servers.clone()) {
servers_list.append(&build_sidebar_button(
ctx.clone(),
&split_view,
server,
&servers_list,
));
}
sidebar.append(&servers_list);
let add_server = Button::builder()
.label("Add Server")
// .start_icon_name("list-add-symbolic")
.margin_top(10)
.build();
add_server.connect_clicked(clone!(
#[weak]
app,
#[weak]
servers_list,
#[weak]
ctx,
#[weak]
split_view,
move |_| {
let dialog = Dialog::new();
let vbox = GtkBox::new(Orientation::Vertical, 5);
vbox.set_margin_bottom(20);
vbox.set_margin_top(20);
vbox.set_margin_end(20);
vbox.set_margin_start(20);
vbox.append(&Label::builder().label("Add server").build());
let entry = Entry::builder().placeholder_text("Server host").build();
vbox.append(&entry);
let hbox = GtkBox::new(Orientation::Horizontal, 5);
let confirm = Button::builder().label("Confirm").build();
confirm.connect_clicked(clone!(
#[weak]
dialog,
#[weak]
servers_list,
#[weak]
ctx,
#[weak]
split_view,
#[weak]
entry,
move |_| {
let server: String = entry.text().into();
let mut config = ctx.config.read().unwrap().clone();
config.servers.push(server.clone());
ctx.set_config(&config);
try_save_config(get_config_path(), &config);
servers_list.append(&build_sidebar_button(
ctx.clone(),
&split_view,
server,
&servers_list,
));
dialog.close();
}
));
hbox.append(&confirm);
let cancel = Button::builder().label("Cancel").build();
cancel.connect_clicked(clone!(
#[weak]
dialog,
move |_| {
dialog.close();
}
));
hbox.append(&cancel);
vbox.append(&hbox);
dialog.set_child(Some(&vbox));
dialog.present(app.active_window().as_ref());
}
));
sidebar.append(&add_server);
sidebar
}
@ -185,30 +343,26 @@ fn build_ui(ctx: Arc<Context>, app: &Application) -> UiModel {
let main_box = GtkBox::new(Orientation::Vertical, 0);
let title = format!(
"bRAC - Connected to {} as {}",
ctx.config(|o| o.host.clone()),
&ctx.name()
);
let (header, page, chat_box, chat_scrolled) = build_page(ctx.clone(), app);
let sidebar = build_sidebar(ctx.clone(), &app);
let split_view = OverlaySplitView::builder()
.sidebar(&sidebar)
.content(&page)
.enable_hide_gesture(true)
.enable_show_gesture(true)
.collapsed(true)
.build();
let sidebar = build_sidebar(ctx.clone(), &app, &split_view);
split_view.set_sidebar(Some(&sidebar));
main_box.append(&split_view);
let toggle_button = Button::from_icon_name("go-previous-symbolic");
toggle_button.connect_clicked(clone!(
#[weak] split_view,
#[weak]
split_view,
move |_| {
split_view.set_show_sidebar(!split_view.shows_sidebar());
}
@ -218,7 +372,11 @@ fn build_ui(ctx: Arc<Context>, app: &Application) -> UiModel {
let window = ApplicationWindow::builder()
.application(app)
.title(&title)
.title(&format!(
"bRAC - Connected to {} as {}",
ctx.config(|o| o.host.clone()),
&ctx.name()
))
.default_width(500)
.default_height(500)
.resizable(true)
@ -226,13 +384,11 @@ fn build_ui(ctx: Arc<Context>, app: &Application) -> UiModel {
.content(&main_box)
.build();
let breakpoint = Breakpoint::new(
BreakpointCondition::new_length(
let breakpoint = Breakpoint::new(BreakpointCondition::new_length(
libadwaita::BreakpointConditionLengthType::MinWidth,
700.0,
libadwaita::LengthUnit::Px
)
);
libadwaita::LengthUnit::Px,
));
breakpoint.add_setter(&split_view, "collapsed", Some(&false.into()));
breakpoint.add_setter(&toggle_button, "visible", Some(&false.into()));
@ -252,7 +408,7 @@ fn build_ui(ctx: Arc<Context>, app: &Application) -> UiModel {
#[cfg(all(not(feature = "libnotify"), not(feature = "notify-rust")))]
notifications: Arc::new(RwLock::new(Vec::<String>::new())),
avatars: Arc::new(Mutex::new(HashMap::new())),
latest_sign: Arc::new(AtomicU64::new(0))
latest_sign: Arc::new(AtomicU64::new(0)),
}
}
@ -332,19 +488,38 @@ fn setup(_: &Application, ctx: Arc<Context>, ui: UiModel) {
thread::spawn(move || {
for message in messages.iter() {
let Some(avatar_url) = grab_avatar(message) else { continue };
let Some(avatar_url) = grab_avatar(message) else {
continue;
};
let avatar_id = get_avatar_id(&avatar_url);
let Some(avatar) = load_avatar(&avatar_url, ctx.config(|o| o.proxy.clone()), 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 Some(avatar) = load_avatar(
&avatar_url,
ctx.config(|o| o.proxy.clone()),
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() {
if let Some(pics) = ui.avatars.lock().unwrap().remove(&avatar_id) {
if let Some(pics) =
ui.avatars.lock().unwrap().remove(&avatar_id)
{
for pic in pics {
pic.set_custom_image(Some(&texture));
}
@ -457,13 +632,13 @@ fn load_avatar(url: &str, proxy: Option<String>, response_limit: usize) -> Optio
reqwest::blocking::Client::builder()
.proxy(reqwest::Proxy::all(&proxy).ok()?)
.build().ok()?
.build()
.ok()?
} else {
reqwest::blocking::Client::new()
};
client.get(url).send().ok()
.and_then(|mut resp| {
client.get(url).send().ok().and_then(|mut resp| {
let mut data = Vec::new();
let mut length = 0;
@ -489,7 +664,7 @@ fn load_avatar(url: &str, proxy: Option<String>, response_limit: usize) -> Optio
fn get_message_sign(name: &str, date: &str) -> u64 {
let mut hasher = DefaultHasher::new();
hasher.write(name.as_bytes());
hasher.write(date[..date.len()-2].as_bytes());
hasher.write(date[..date.len() - 2].as_bytes());
hasher.finish()
}
@ -512,9 +687,21 @@ fn on_add_message(ctx: Arc<Context>, ui: &UiModel, message: String, notify: bool
}
if ctx.config(|o| o.new_ui_enabled) {
ui.chat_box.append(&get_new_message_box(ctx.clone(), ui, message, notify, formatting_enabled));
ui.chat_box.append(&get_new_message_box(
ctx.clone(),
ui,
message,
notify,
formatting_enabled,
));
} else {
ui.chat_box.append(&get_message_box(ctx.clone(), ui, message, notify, formatting_enabled));
ui.chat_box.append(&get_message_box(
ctx.clone(),
ui,
message,
notify,
formatting_enabled,
));
};
timeout_add_local_once(Duration::from_millis(1000), move || {

View File

@ -1,35 +1,29 @@
use std::sync::Arc;
use libadwaita::gtk::Adjustment;
use libadwaita::{
self as adw, ActionRow, ButtonRow, EntryRow, PreferencesDialog, PreferencesGroup, PreferencesPage, SpinRow, SwitchRow
};
use adw::gdk::Display;
use adw::glib::clone;
use adw::glib::{
self,
};
use adw::glib::{self};
use adw::prelude::*;
use adw::Application;
use libadwaita::gtk::Adjustment;
use libadwaita::{
self as adw, ActionRow, ButtonRow, EntryRow, PreferencesDialog, PreferencesGroup,
PreferencesPage, SpinRow, SwitchRow,
};
use adw::gtk;
use gtk::Button;
use crate::chat::{
config::{
get_config_path, Config,
},
config::{get_config_path, Config},
ctx::Context,
};
use super::{try_save_config, update_window_title};
pub fn open_settings(ctx: Arc<Context>, app: &Application) {
let dialog = PreferencesDialog::builder().build();
let page = PreferencesPage::builder()
.title("General")
.icon_name("avatar-default-symbolic")
@ -40,7 +34,6 @@ pub fn open_settings(ctx: Arc<Context>, app: &Application) {
.description("Profile preferences")
.build();
// Name preference
let name = EntryRow::builder()
@ -50,7 +43,6 @@ pub fn open_settings(ctx: Arc<Context>, app: &Application) {
group.add(&name);
// Avatar preference
let avatar = EntryRow::builder()
@ -60,17 +52,13 @@ pub fn open_settings(ctx: Arc<Context>, app: &Application) {
group.add(&avatar);
page.add(&group);
let group = PreferencesGroup::builder()
.title("Server")
.description("Connection preferences")
.build();
// Host preference
let host = EntryRow::builder()
@ -80,60 +68,61 @@ pub fn open_settings(ctx: Arc<Context>, app: &Application) {
group.add(&host);
// Messages limit preference
let messages_limit = SpinRow::builder()
.title("Messages limit")
.adjustment(&Adjustment::builder()
.adjustment(
&Adjustment::builder()
.lower(1.0)
.upper(1048576.0)
.page_increment(10.0)
.step_increment(10.0)
.value(ctx.config(|o| o.max_messages) as f64)
.build())
.build(),
)
.build();
group.add(&messages_limit);
// Update interval preference
let update_interval = SpinRow::builder()
.title("Update interval")
.subtitle("In milliseconds")
.adjustment(&Adjustment::builder()
.adjustment(
&Adjustment::builder()
.lower(10.0)
.upper(1048576.0)
.page_increment(10.0)
.step_increment(10.0)
.value(ctx.config(|o| o.update_time) as f64)
.build())
.build(),
)
.build();
group.add(&update_interval);
// Update interval OOF preference
let update_interval_oof = SpinRow::builder()
.title("Update interval when unfocused")
.subtitle("In milliseconds")
.adjustment(&Adjustment::builder()
.adjustment(
&Adjustment::builder()
.lower(10.0)
.upper(1048576.0)
.page_increment(10.0)
.step_increment(10.0)
.value(ctx.config(|o| o.oof_update_time) as f64)
.build())
.build(),
)
.build();
group.add(&update_interval_oof);
page.add(&group);
let group = PreferencesGroup::builder()
.title("Config")
.description("Configuration tools")
@ -154,7 +143,8 @@ pub fn open_settings(ctx: Arc<Context>, app: &Application) {
config_path_copy.set_margin_top(10);
config_path_copy.set_margin_bottom(10);
config_path_copy.connect_clicked(clone!(
#[weak] clipboard,
#[weak]
clipboard,
move |_| {
if let Some(text) = get_config_path().to_str() {
clipboard.set_text(text);
@ -169,14 +159,15 @@ pub fn open_settings(ctx: Arc<Context>, app: &Application) {
// Reset button
let reset_button = ButtonRow::builder()
.title("Reset all")
.build();
let reset_button = ButtonRow::builder().title("Reset all").build();
reset_button.connect_activated(clone!(
#[weak] ctx,
#[weak] app,
#[weak] dialog,
#[weak]
ctx,
#[weak]
app,
#[weak]
dialog,
move |_| {
dialog.close();
let config = Config::default();
@ -192,8 +183,6 @@ pub fn open_settings(ctx: Arc<Context>, app: &Application) {
dialog.add(&page);
let page = PreferencesPage::builder()
.title("Protocol")
.icon_name("network-wired-symbolic")
@ -204,7 +193,6 @@ pub fn open_settings(ctx: Arc<Context>, app: &Application) {
.description("Network preferences")
.build();
// Proxy preference
let proxy = EntryRow::builder()
@ -214,33 +202,31 @@ pub 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()
.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(),
)
.build();
group.add(&max_avatar_size);
page.add(&group);
let group = PreferencesGroup::builder()
.title("Protocol")
.description("Rac protocol preferences")
.build();
// Message format preference
let message_format = EntryRow::builder()
@ -252,7 +238,6 @@ pub fn open_settings(ctx: Arc<Context>, app: &Application) {
page.add(&group);
// Hide IP preference
let hide_my_ip = SwitchRow::builder()
@ -263,7 +248,6 @@ pub fn open_settings(ctx: Arc<Context>, app: &Application) {
group.add(&hide_my_ip);
// Chunked reading preference
let chunked_reading = SwitchRow::builder()
@ -274,7 +258,6 @@ pub fn open_settings(ctx: Arc<Context>, app: &Application) {
group.add(&chunked_reading);
// Enable commands preference
let enable_commands = SwitchRow::builder()
@ -285,12 +268,10 @@ pub fn open_settings(ctx: Arc<Context>, app: &Application) {
group.add(&enable_commands);
page.add(&group);
dialog.add(&page);
let page = PreferencesPage::builder()
.title("Interface")
.icon_name("applications-graphics-symbolic")
@ -301,7 +282,6 @@ pub fn open_settings(ctx: Arc<Context>, app: &Application) {
.description("Messages render preferences")
.build();
// Debug logs preference
let debug_logs = SwitchRow::builder()
@ -312,7 +292,6 @@ pub fn open_settings(ctx: Arc<Context>, app: &Application) {
group.add(&debug_logs);
// Show IPs preference
let show_ips = SwitchRow::builder()
@ -323,7 +302,6 @@ pub fn open_settings(ctx: Arc<Context>, app: &Application) {
group.add(&show_ips);
// Format messages preference
let format_messages = SwitchRow::builder()
@ -334,7 +312,6 @@ pub fn open_settings(ctx: Arc<Context>, app: &Application) {
group.add(&format_messages);
// Show avatars preference
let show_avatars = SwitchRow::builder()
@ -346,13 +323,11 @@ pub fn open_settings(ctx: Arc<Context>, app: &Application) {
group.add(&show_avatars);
page.add(&group);
let group = PreferencesGroup::builder()
.title("Interface")
.description("General interface preferences (restart after changing)")
.build();
// Remove GUI shit preference
let remove_gui_shit = SwitchRow::builder()
@ -363,24 +338,24 @@ pub fn open_settings(ctx: Arc<Context>, app: &Application) {
group.add(&remove_gui_shit);
// Konata size preference
let konata_size = SpinRow::builder()
.title("Konata size")
.subtitle("Set konata size percent")
.adjustment(&Adjustment::builder()
.adjustment(
&Adjustment::builder()
.lower(0.0)
.upper(200.0)
.page_increment(10.0)
.step_increment(10.0)
.value(ctx.config(|o| o.konata_size) as f64)
.build())
.build(),
)
.build();
group.add(&konata_size);
// Enable notifications preference
let enable_notifications = SwitchRow::builder()
@ -392,11 +367,11 @@ pub fn open_settings(ctx: Arc<Context>, app: &Application) {
group.add(&enable_notifications);
page.add(&group);
dialog.add(&page);
dialog.connect_closed(move |_| {
let old_config = ctx.config.read().unwrap().clone();
let config = Config {
host: host.text().to_string(),
name: {
@ -441,6 +416,7 @@ pub fn open_settings(ctx: Arc<Context>, app: &Application) {
Some(proxy)
}
},
servers: old_config.servers,
};
ctx.set_config(&config);
try_save_config(get_config_path(), &config);
@ -449,5 +425,3 @@ pub fn open_settings(ctx: Arc<Context>, app: &Application) {
dialog.present(app.active_window().as_ref());
}