root/src/thumb.rs

// thumb.rs
//
// Copyright 2020-2024 nee <nee-git@hidamari.blue>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
//
// SPDX-License-Identifier: GPL-3.0-or-later
use crate::booru::Post;
use crate::data::*;
use crate::send;

use gtk::glib;
use gtk::glib::{Object, ParamSpec, Properties, Value};
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use std::cell::RefCell;
use std::rc::Rc;
use tokio::sync::mpsc::Sender;

gtk::glib::wrapper! {
    pub struct ThumbObject(ObjectSubclass<imp::ThumbObject>);
}
impl ThumbObject {
    pub fn new(texture: Option<gtk::gdk::Texture>, post: Box<dyn Post>, viewed: bool) -> Self {
        let self_: Self = gtk::glib::Object::new();

        let image = match texture.as_ref() {
            Some(t) => gtk::Image::from_paintable(Some(t)),
            None => gtk::Image::from_icon_name("missing-symbolic"),
        };
        image.set_pixel_size(crate::DEFAULT_THUMB_SIZE_UI);

        let s = imp::ThumbObject::from_obj(&self_);
        {
            let mut data = s.data.borrow_mut();
            data.image = Some(image);
            data.post = Some(post);
            data.texture = texture;
        }
        *s.viewed.borrow_mut() = viewed;
        self_
    }

    pub fn object_data(&self) -> Rc<RefCell<ThumbData>> {
        let self_ = imp::ThumbObject::from_obj(self);
        self_.data.clone()
    }
}

#[derive(Default)]
pub struct ThumbData {
    pub image: Option<gtk::Image>,
    pub post: Option<Box<dyn Post>>,
    pub texture: Option<gtk::gdk::Texture>,
}

mod imp {
    use super::*;
    // Object holding the state
    #[derive(Default, Properties)]
    #[properties(wrapper_type = super::ThumbObject)]
    pub struct ThumbObject {
        pub data: Rc<RefCell<ThumbData>>,
        #[property(get, set)]
        pub viewed: Rc<RefCell<bool>>,
    }

    // The central trait for subclassing a GObject
    #[glib::object_subclass]
    impl ObjectSubclass for ThumbObject {
        const NAME: &'static str = "ThumbObject";
        type Type = super::ThumbObject;

        fn new() -> Self {
            Self {
                data: Rc::new(RefCell::new(ThumbData::default())),
                viewed: Rc::new(RefCell::new(false)),
            }
        }
    }

    impl ObjectImpl for ThumbObject {
        fn properties() -> &'static [ParamSpec] {
            Self::derived_properties()
        }

        fn set_property(&self, id: usize, value: &Value, pspec: &ParamSpec) {
            self.derived_set_property(id, value, pspec)
        }

        fn property(&self, id: usize, pspec: &ParamSpec) -> Value {
            self.derived_property(id, pspec)
        }
    }
}

pub fn init_thumb(
    thumb_object: &ThumbObject,
    overlay: &gtk::Overlay,
    sender: &Sender<Action>,
    img: &gtk::Image,
    post: Box<dyn Post>,
    thumb_size: u32,
) {
    // let img = thumb_from_binary(bytes, &post);
    // TODO why is this called so often?
    let button = gtk::Button::new();
    let download_button = gtk::Button::new();
    let download_button_img = gtk::Image::from_icon_name("document-save-symbolic");
    download_button.add_css_class("flat");
    download_button.set_child(Some(&download_button_img));
    download_button.set_halign(gtk::Align::End);
    download_button.set_valign(gtk::Align::Start);
    let sender2 = sender.clone();
    let download_post = post.clone_post();
    download_button.connect_clicked(move |_| {
        let cmd = Action::DownloadImage {
            post: download_post.clone_post(),
        };
        send!(sender2, cmd);
    });

    let old_overlay = gtk::Box::new(gtk::Orientation::Vertical, 0);
    old_overlay.add_css_class("old");
    old_overlay.set_can_target(false);
    old_overlay.set_visible(thumb_object.viewed());

    overlay.add_overlay(&old_overlay);
    thumb_object
        .bind_property("viewed", &old_overlay, "visible")
        .build();
    thumb_object
        .bind_property("viewed", &old_overlay, "visible")
        .build();
    // println!("BOUND FOR {}", thumb_object.object_data().borrow().post.as_ref().unwrap().id());

    if !post.is_local() {
        overlay.add_overlay(&download_button);
    }

    button.set_child(Some(img));

    button.set_size_request(thumb_size as i32, thumb_size as i32);
    button.set_hexpand(true);
    button.set_has_frame(false);
    button.set_tooltip_text(Some(&post.tags().join(" ")));
    overlay.set_child(Some(&button));

    let sender = sender.clone();
    button.connect_clicked(move |_| {
        let cmd = Action::OpenImage {
            post: post.clone_post(),
        };
        send!(sender, cmd);
    });
}

pub fn uninit_thumb(overlay: &gtk::Overlay) {
    overlay.set_child(gtk::Widget::NONE);
    let children = overlay.observe_children();
    let cs: Vec<_> = children
        .iter::<Object>()
        .filter_map(|c| {
            if let Ok(c) = c {
                Some(c.downcast::<gtk::Widget>().unwrap().clone())
            } else {
                None
            }
        })
        .collect();
    for c in cs {
        overlay.remove_overlay(&c);
    }
}