mirror of
https://github.com/MeexReay/bRAC.git
synced 2025-05-06 13:38:04 +03:00
464 lines
13 KiB
Rust
464 lines
13 KiB
Rust
use std::sync::{Arc, RwLock};
|
|
use std::time::Duration;
|
|
|
|
use colored::{Color, Colorize};
|
|
use gtk4::gdk::Display;
|
|
use gtk4::gdk_pixbuf::PixbufLoader;
|
|
use gtk4::glib::clone::Downgrade;
|
|
use gtk4::glib::{idle_add_local, idle_add_local_once, ControlFlow, source::timeout_add_once};
|
|
use gtk4::{glib, glib::clone, Align, Box as GtkBox, Label, ScrolledWindow};
|
|
use gtk4::{CssProvider, Entry, Orientation, Overlay, Picture};
|
|
use gtk4::prelude::*;
|
|
use gtk4::{Application, ApplicationWindow, Button};
|
|
use std::sync::mpsc::{channel, Sender, Receiver};
|
|
use std::error::Error;
|
|
use std::thread;
|
|
use std::cell::RefCell;
|
|
|
|
use crate::config::Context;
|
|
use crate::proto::{connect, read_messages};
|
|
|
|
use super::{format_message, on_send_message, parse_message, set_chat, ChatStorage};
|
|
|
|
|
|
pub struct ChatContext {
|
|
pub messages: Arc<ChatStorage>,
|
|
pub registered: Arc<RwLock<Option<String>>>,
|
|
pub sender: Sender<String>
|
|
}
|
|
|
|
struct UiModel {
|
|
chat_box: GtkBox,
|
|
chat_scrolled: ScrolledWindow
|
|
}
|
|
|
|
thread_local!(
|
|
static GLOBAL: RefCell<Option<(UiModel, Receiver<String>)>> = RefCell::new(None);
|
|
);
|
|
|
|
pub fn add_chat_message(ctx: Arc<Context>, message: String) {
|
|
let _ = ctx.chat().sender.send(message);
|
|
}
|
|
|
|
pub fn print_message(ctx: Arc<Context>, message: String) -> Result<(), Box<dyn Error>> {
|
|
ctx.chat().messages.append(ctx.max_messages, vec![message.clone()]);
|
|
add_chat_message(ctx.clone(), message);
|
|
Ok(())
|
|
}
|
|
|
|
pub fn recv_tick(ctx: Arc<Context>) -> Result<(), Box<dyn Error>> {
|
|
match read_messages(
|
|
&mut connect(&ctx.host, ctx.enable_ssl)?,
|
|
ctx.max_messages,
|
|
ctx.chat().messages.packet_size(),
|
|
!ctx.enable_ssl,
|
|
ctx.enable_chunked
|
|
) {
|
|
Ok(Some((messages, size))) => {
|
|
let messages: Vec<String> = if ctx.disable_formatting {
|
|
messages
|
|
} else {
|
|
messages.into_iter().flat_map(|o| format_message(ctx.enable_ip_viewing, o)).collect()
|
|
};
|
|
|
|
if ctx.enable_chunked {
|
|
ctx.chat().messages.append_and_store(ctx.max_messages, messages.clone(), size);
|
|
for msg in messages {
|
|
add_chat_message(ctx.clone(), msg.clone());
|
|
}
|
|
} else {
|
|
ctx.chat().messages.update(ctx.max_messages, messages.clone(), size);
|
|
for msg in messages {
|
|
add_chat_message(ctx.clone(), msg.clone());
|
|
}
|
|
}
|
|
},
|
|
Err(e) => {
|
|
let msg = format!("Read messages error: {}", e.to_string()).bright_red().to_string();
|
|
ctx.chat().messages.append(ctx.max_messages, vec![msg.clone()]);
|
|
add_chat_message(ctx.clone(), msg.clone());
|
|
}
|
|
_ => {}
|
|
}
|
|
thread::sleep(Duration::from_millis(ctx.update_time as u64));
|
|
Ok(())
|
|
}
|
|
|
|
fn build_ui(ctx: Arc<Context>, app: &Application) {
|
|
let main_box = GtkBox::new(Orientation::Vertical, 5);
|
|
|
|
main_box.set_margin_bottom(5);
|
|
main_box.set_margin_end(5);
|
|
main_box.set_margin_start(5);
|
|
main_box.set_margin_top(5);
|
|
|
|
let chat_box = GtkBox::new(Orientation::Vertical, 2);
|
|
|
|
let chat_scrolled = ScrolledWindow::builder()
|
|
.child(&chat_box)
|
|
.vexpand(true)
|
|
.hexpand(true)
|
|
.propagate_natural_height(true)
|
|
.build();
|
|
|
|
main_box.append(&chat_scrolled);
|
|
|
|
let send_box = GtkBox::new(Orientation::Horizontal, 5);
|
|
|
|
let text_entry = Entry::builder()
|
|
.placeholder_text("Message")
|
|
.hexpand(true)
|
|
.build();
|
|
|
|
send_box.append(&text_entry);
|
|
|
|
let send_btn = Button::builder()
|
|
.label("Send")
|
|
.build();
|
|
|
|
send_btn.connect_clicked(clone!(
|
|
#[weak] text_entry,
|
|
#[weak] ctx,
|
|
move |_| {
|
|
idle_add_local_once(clone!(
|
|
#[weak] text_entry,
|
|
move || {
|
|
text_entry.set_text("");
|
|
}
|
|
));
|
|
|
|
if let Err(e) = on_send_message(ctx.clone(), &text_entry.text()) {
|
|
let msg = format!("Send message error: {}", e.to_string()).bright_red().to_string();
|
|
add_chat_message(ctx.clone(), msg);
|
|
}
|
|
}
|
|
));
|
|
|
|
text_entry.connect_activate(clone!(
|
|
#[weak] text_entry,
|
|
#[weak] ctx,
|
|
move |_| {
|
|
idle_add_local_once(clone!(
|
|
#[weak] text_entry,
|
|
move || {
|
|
text_entry.set_text("");
|
|
}
|
|
));
|
|
|
|
if let Err(e) = on_send_message(ctx.clone(), &text_entry.text()) {
|
|
let msg = format!("Send message error: {}", e.to_string()).bright_red().to_string();
|
|
add_chat_message(ctx.clone(), msg);
|
|
}
|
|
}
|
|
));
|
|
|
|
send_box.append(&send_btn);
|
|
|
|
main_box.append(&send_box);
|
|
|
|
let scrolled_window_weak = Downgrade::downgrade(&chat_scrolled);
|
|
|
|
idle_add_local({
|
|
let scrolled_window_weak = scrolled_window_weak.clone();
|
|
|
|
move || {
|
|
if let Some(o) = scrolled_window_weak.upgrade() {
|
|
o.vadjustment().set_value(o.vadjustment().upper() - o.vadjustment().page_size());
|
|
}
|
|
ControlFlow::Break
|
|
}
|
|
});
|
|
|
|
let overlay = Overlay::new();
|
|
|
|
overlay.set_child(Some(&main_box));
|
|
|
|
let bytes = include_bytes!("../../brac_logo.png");
|
|
let loader = PixbufLoader::new();
|
|
loader.write(bytes).unwrap();
|
|
loader.close().unwrap();
|
|
let pixbuf = loader.pixbuf().unwrap();
|
|
|
|
let logo = Picture::for_pixbuf(&pixbuf);
|
|
logo.set_size_request(500, 189);
|
|
logo.set_can_target(false);
|
|
logo.set_can_focus(false);
|
|
logo.set_halign(Align::End);
|
|
logo.set_valign(Align::Start);
|
|
|
|
overlay.add_overlay(&logo);
|
|
|
|
let window = ApplicationWindow::builder()
|
|
.application(app)
|
|
.title(format!("bRAC - Connected to {} as {}", &ctx.host, &ctx.name))
|
|
.default_width(500)
|
|
.default_height(500)
|
|
.resizable(false)
|
|
.decorated(true)
|
|
.child(&overlay)
|
|
.build();
|
|
|
|
window.connect_default_width_notify({
|
|
let scrolled_window_weak = scrolled_window_weak.clone();
|
|
|
|
move |_| {
|
|
let scrolled_window_weak = scrolled_window_weak.clone();
|
|
idle_add_local(move || {
|
|
if let Some(o) = scrolled_window_weak.upgrade() {
|
|
o.vadjustment().set_value(o.vadjustment().upper() - o.vadjustment().page_size());
|
|
}
|
|
ControlFlow::Break
|
|
});
|
|
}
|
|
});
|
|
|
|
window.show();
|
|
|
|
let ui = UiModel {
|
|
chat_scrolled,
|
|
chat_box
|
|
};
|
|
|
|
setup(ctx.clone(), ui);
|
|
load_css();
|
|
}
|
|
|
|
fn setup(ctx: Arc<Context>, ui: UiModel) {
|
|
let (sender, receiver) = channel();
|
|
|
|
set_chat(ctx.clone(), ChatContext {
|
|
messages: Arc::new(ChatStorage::new()),
|
|
registered: Arc::new(RwLock::new(None)),
|
|
sender
|
|
});
|
|
|
|
thread::spawn({
|
|
let ctx = ctx.clone();
|
|
|
|
move || {
|
|
loop {
|
|
if let Err(e) = recv_tick(ctx.clone()) {
|
|
let _ = print_message(ctx.clone(), format!("Print messages error: {}", e.to_string()).bright_red().to_string());
|
|
thread::sleep(Duration::from_secs(1));
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
let (tx, rx) = channel();
|
|
|
|
GLOBAL.with(|global| {
|
|
*global.borrow_mut() = Some((ui, rx));
|
|
});
|
|
|
|
thread::spawn({
|
|
let ctx = ctx.clone();
|
|
move || {
|
|
while let Ok(message) = receiver.recv() {
|
|
let _ = tx.send(message.clone());
|
|
let ctx = ctx.clone();
|
|
glib::source::timeout_add_once(Duration::ZERO, move || {
|
|
GLOBAL.with(|global| {
|
|
if let Some((ui, rx)) = &*global.borrow() {
|
|
let message: String = rx.recv().unwrap();
|
|
on_add_message(ctx.clone(), &ui, message);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
fn load_css() {
|
|
let provider = CssProvider::new();
|
|
provider.load_from_data("
|
|
|
|
.message-content {
|
|
color: #FFFFFF;
|
|
}
|
|
|
|
.message-date {
|
|
color: #AAAAAA;
|
|
}
|
|
|
|
.message-ip {
|
|
color: #AAAAAA;
|
|
}
|
|
|
|
.message-name {
|
|
font-weight: bold;
|
|
}
|
|
|
|
.message-name-black {
|
|
color: #2E2E2E; /* Темный черный */
|
|
}
|
|
|
|
.message-name-red {
|
|
color: #8B0000; /* Темный красный */
|
|
}
|
|
|
|
.message-name-green {
|
|
color: #006400; /* Темный зеленый */
|
|
}
|
|
|
|
.message-name-yellow {
|
|
color: #8B8B00; /* Темный желтый */
|
|
}
|
|
|
|
.message-name-blue {
|
|
color: #00008B; /* Темный синий */
|
|
}
|
|
|
|
.message-name-magenta {
|
|
color: #8B008B; /* Темный пурпурный */
|
|
}
|
|
|
|
.message-name-cyan {
|
|
color: #008B8B; /* Темный бирюзовый */
|
|
}
|
|
|
|
.message-name-white {
|
|
color: #A9A9A9; /* Темный белый */
|
|
}
|
|
|
|
.message-name-bright-black {
|
|
color: #555555; /* Яркий черный */
|
|
}
|
|
|
|
.message-name-bright-red {
|
|
color: #FF0000; /* Яркий красный */
|
|
}
|
|
|
|
.message-name-bright-green {
|
|
color: #00FF00; /* Яркий зеленый */
|
|
}
|
|
|
|
.message-name-bright-yellow {
|
|
color: #FFFF00; /* Яркий желтый */
|
|
}
|
|
|
|
.message-name-bright-blue {
|
|
color: #0000FF; /* Яркий синий */
|
|
}
|
|
|
|
.message-name-bright-magenta {
|
|
color: #FF00FF; /* Яркий пурпурный */
|
|
}
|
|
|
|
.message-name-bright-cyan {
|
|
color: #00FFFF; /* Яркий бирюзовый */
|
|
}
|
|
|
|
.message-name-bright-white {
|
|
color: #FFFFFF; /* Яркий белый */
|
|
}
|
|
|
|
");
|
|
|
|
gtk4::style_context_add_provider_for_display(
|
|
&Display::default().expect("Could not connect to a display."),
|
|
&provider,
|
|
gtk4::STYLE_PROVIDER_PRIORITY_APPLICATION,
|
|
);
|
|
}
|
|
|
|
fn on_add_message(ctx: Arc<Context>, ui: &UiModel, message: String) {
|
|
if let Some((date, ip, content, nick)) = parse_message(message.clone()) {
|
|
let hbox = GtkBox::new(Orientation::Horizontal, 2);
|
|
|
|
if let Some(ip) = ip {
|
|
if ctx.enable_ip_viewing {
|
|
let ip = Label::builder()
|
|
.label(ip)
|
|
.margin_end(10)
|
|
.halign(Align::Start)
|
|
.css_classes(["message-ip"])
|
|
.build();
|
|
|
|
hbox.append(&ip);
|
|
}
|
|
}
|
|
|
|
let date = Label::builder()
|
|
.label(format!("[{date}]"))
|
|
.halign(Align::Start)
|
|
.css_classes(["message-date"])
|
|
.build();
|
|
|
|
hbox.append(&date);
|
|
|
|
if let Some((name, color)) = nick {
|
|
let color = match color {
|
|
Color::Black => "black",
|
|
Color::Red => "red",
|
|
Color::Green => "green",
|
|
Color::Yellow => "yellow",
|
|
Color::Blue => "blue",
|
|
Color::Magenta => "magenta",
|
|
Color::Cyan => "cyan",
|
|
Color::White => "white",
|
|
Color::BrightBlack => "bright-black",
|
|
Color::BrightRed => "bright-red",
|
|
Color::BrightGreen => "bright-green",
|
|
Color::BrightYellow => "bright-yellow",
|
|
Color::BrightBlue => "bright-blue",
|
|
Color::BrightMagenta => "bright-magenta",
|
|
Color::BrightCyan => "bright-cyan",
|
|
Color::BrightWhite => "bright-white",
|
|
_ => "unknown"
|
|
};
|
|
|
|
let name = Label::builder()
|
|
.label(format!("<{name}>"))
|
|
.halign(Align::Start)
|
|
.css_classes(["message-name", &format!("message-name-{}", color)])
|
|
.build();
|
|
|
|
hbox.append(&name);
|
|
}
|
|
|
|
let content = Label::builder()
|
|
.label(content)
|
|
.halign(Align::Start)
|
|
.css_classes(["message-content"])
|
|
.build();
|
|
|
|
hbox.append(&content);
|
|
|
|
ui.chat_box.append(&hbox);
|
|
} else {
|
|
let content = Label::builder()
|
|
.label(message)
|
|
.halign(Align::Start)
|
|
.css_classes(["message-content"])
|
|
.build();
|
|
|
|
ui.chat_box.append(&content);
|
|
}
|
|
|
|
timeout_add_once(Duration::from_millis(10), move || {
|
|
GLOBAL.with(|global| {
|
|
if let Some((ui, _)) = &*global.borrow() {
|
|
let o = &ui.chat_scrolled;
|
|
o.vadjustment().set_value(o.vadjustment().upper() - o.vadjustment().page_size());
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
pub fn run_main_loop(ctx: Arc<Context>) {
|
|
let application = Application::builder()
|
|
.application_id("ru.themixray.bRAC")
|
|
.build();
|
|
|
|
application.connect_activate({
|
|
let ctx = ctx.clone();
|
|
|
|
move |app| {
|
|
build_ui(ctx.clone(), app);
|
|
}
|
|
});
|
|
|
|
application.run();
|
|
} |