Show image attachment
This commit is contained in:
@ -36,9 +36,9 @@ pub struct Message {
|
|||||||
pub tags: Vec<String>,
|
pub tags: Vec<String>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub priority: Option<i8>,
|
pub priority: Option<i8>,
|
||||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub attach: Vec<String>,
|
pub attachment: Option<Attachment>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub icon: Option<String>,
|
pub icon: Option<String>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
@ -109,6 +109,28 @@ pub struct MinMessage {
|
|||||||
pub time: u64,
|
pub time: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct Attachment {
|
||||||
|
pub name: String,
|
||||||
|
pub url: url::Url,
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub atype: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub size: Option<usize>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub expires: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Attachment {
|
||||||
|
pub fn is_image(&self) -> bool {
|
||||||
|
let Some(ext) = self.name.split('.').last() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
["jpeg", "jpg", "png", "webp", "gif"].contains(&ext)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Subscription {
|
pub struct Subscription {
|
||||||
pub server: String,
|
pub server: String,
|
||||||
|
|||||||
@ -1,8 +1,13 @@
|
|||||||
|
use std::io::Read;
|
||||||
|
|
||||||
use adw::prelude::*;
|
use adw::prelude::*;
|
||||||
use adw::subclass::prelude::*;
|
use adw::subclass::prelude::*;
|
||||||
use chrono::NaiveDateTime;
|
use chrono::NaiveDateTime;
|
||||||
use gtk::glib;
|
use gdk_pixbuf::Pixbuf;
|
||||||
|
use gtk::gdk_pixbuf;
|
||||||
|
use gtk::{gdk, gio, glib};
|
||||||
use ntfy_daemon::models;
|
use ntfy_daemon::models;
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
mod imp {
|
mod imp {
|
||||||
use super::*;
|
use super::*;
|
||||||
@ -41,6 +46,7 @@ impl MessageRow {
|
|||||||
self.set_margin_end(8);
|
self.set_margin_end(8);
|
||||||
self.set_column_spacing(8);
|
self.set_column_spacing(8);
|
||||||
self.set_row_spacing(8);
|
self.set_row_spacing(8);
|
||||||
|
let mut row = 0;
|
||||||
|
|
||||||
let time = gtk::Label::builder()
|
let time = gtk::Label::builder()
|
||||||
.label(
|
.label(
|
||||||
@ -51,7 +57,7 @@ impl MessageRow {
|
|||||||
.xalign(0.0)
|
.xalign(0.0)
|
||||||
.build();
|
.build();
|
||||||
time.add_css_class("caption");
|
time.add_css_class("caption");
|
||||||
self.attach(&time, 0, 0, 1, 1);
|
self.attach(&time, 0, row, 1, 1);
|
||||||
|
|
||||||
if let Some(p) = msg.priority {
|
if let Some(p) = msg.priority {
|
||||||
let text = format!(
|
let text = format!(
|
||||||
@ -76,6 +82,7 @@ impl MessageRow {
|
|||||||
priority.set_halign(gtk::Align::End);
|
priority.set_halign(gtk::Align::End);
|
||||||
self.attach(&priority, 1, 0, 2, 1);
|
self.attach(&priority, 1, 0, 2, 1);
|
||||||
}
|
}
|
||||||
|
row += 1;
|
||||||
|
|
||||||
if let Some(title) = msg.display_title() {
|
if let Some(title) = msg.display_title() {
|
||||||
let label = gtk::Label::builder()
|
let label = gtk::Label::builder()
|
||||||
@ -86,7 +93,8 @@ impl MessageRow {
|
|||||||
.selectable(true)
|
.selectable(true)
|
||||||
.build();
|
.build();
|
||||||
label.add_css_class("heading");
|
label.add_css_class("heading");
|
||||||
self.attach(&label, 0, 1, 3, 1);
|
self.attach(&label, 0, row, 3, 1);
|
||||||
|
row += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(message) = msg.display_message() {
|
if let Some(message) = msg.display_message() {
|
||||||
@ -98,7 +106,15 @@ impl MessageRow {
|
|||||||
.selectable(true)
|
.selectable(true)
|
||||||
.hexpand(true)
|
.hexpand(true)
|
||||||
.build();
|
.build();
|
||||||
self.attach(&label, 0, 2, 3, 1);
|
self.attach(&label, 0, row, 3, 1);
|
||||||
|
row += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(attachment) = msg.attachment {
|
||||||
|
if attachment.is_image() {
|
||||||
|
self.attach(&self.build_image(attachment.url.to_string()), 0, row, 3, 1);
|
||||||
|
row += 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if msg.actions.len() > 0 {
|
if msg.actions.len() > 0 {
|
||||||
@ -114,7 +130,8 @@ impl MessageRow {
|
|||||||
action_btns.append(&btn);
|
action_btns.append(&btn);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.attach(&action_btns, 0, 3, 3, 1);
|
self.attach(&action_btns, 0, row, 3, 1);
|
||||||
|
row += 1;
|
||||||
}
|
}
|
||||||
if msg.tags.len() > 0 {
|
if msg.tags.len() > 0 {
|
||||||
let mut tags_text = String::from("tags: ");
|
let mut tags_text = String::from("tags: ");
|
||||||
@ -125,9 +142,62 @@ impl MessageRow {
|
|||||||
.wrap(true)
|
.wrap(true)
|
||||||
.wrap_mode(gtk::pango::WrapMode::WordChar)
|
.wrap_mode(gtk::pango::WrapMode::WordChar)
|
||||||
.build();
|
.build();
|
||||||
self.attach(&tags, 0, 4, 3, 1);
|
self.attach(&tags, 0, row, 3, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fn build_image(&self, url: String) -> gtk::Picture {
|
||||||
|
let (tx, rx) = glib::MainContext::channel(Default::default());
|
||||||
|
gio::spawn_blocking(move || {
|
||||||
|
let path = glib::user_cache_dir().join("com.ranfdev.Notify").join(&url);
|
||||||
|
let bytes = if path.exists() {
|
||||||
|
match std::fs::read(&path) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(e) => {
|
||||||
|
error!(error = %e, path = %path.display(), "reading image from disk");
|
||||||
|
return glib::ControlFlow::Break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let res = match ureq::get(&url).call() {
|
||||||
|
Ok(res) => res,
|
||||||
|
Err(e) => {
|
||||||
|
error!(error = %e, "fetching image");
|
||||||
|
return glib::ControlFlow::Break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut bytes = vec![];
|
||||||
|
if let Err(e) = res
|
||||||
|
.into_reader()
|
||||||
|
.take(5 * 1_000_000) // 5 MB
|
||||||
|
.read_to_end(&mut bytes)
|
||||||
|
{
|
||||||
|
error!(error = %e, "reading image data");
|
||||||
|
return glib::ControlFlow::Break;
|
||||||
|
}
|
||||||
|
bytes
|
||||||
|
};
|
||||||
|
|
||||||
|
tx.send(glib::Bytes::from_owned(bytes)).unwrap();
|
||||||
|
glib::ControlFlow::Break
|
||||||
|
});
|
||||||
|
let picture = gtk::Picture::new();
|
||||||
|
picture.set_can_shrink(true);
|
||||||
|
picture.set_height_request(350);
|
||||||
|
let picturec = picture.clone();
|
||||||
|
rx.attach(Default::default(), move |b| {
|
||||||
|
let stream = gio::MemoryInputStream::from_bytes(&b);
|
||||||
|
let pixbuf = match Pixbuf::from_stream(&stream, gio::Cancellable::NONE) {
|
||||||
|
Ok(res) => res,
|
||||||
|
Err(e) => {
|
||||||
|
error!(error = %e, "parsing image contents");
|
||||||
|
return glib::ControlFlow::Break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
picturec.set_paintable(Some(&gdk::Texture::for_pixbuf(&pixbuf)));
|
||||||
|
glib::ControlFlow::Break
|
||||||
|
});
|
||||||
|
picture
|
||||||
|
}
|
||||||
fn build_action_btn(&self, action: models::Action) -> gtk::Button {
|
fn build_action_btn(&self, action: models::Action) -> gtk::Button {
|
||||||
let btn = gtk::Button::new();
|
let btn = gtk::Button::new();
|
||||||
match &action {
|
match &action {
|
||||||
|
|||||||
Reference in New Issue
Block a user