root/src/data.rs

// data.rs
//
// Copyright 2020-2022 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::{Booru, Post, TagDB, WikiEntry};
use crate::browse_page::BrowseAction;
use crate::image_page::ImagePage;
use crate::settings::{BooruData, BooruSettings, Preferences, SavedSearch, SettingsPage};
use tokio::sync::mpsc::Sender;

// use serde::{Deserialize, Serialize};

#[derive(Debug)]
pub enum Action {
    Init,
    GotTagdb {
        tagdb: TagDB,
    },
    OpenBrowse,
    OpenBrowseSearch(String),
    OpenImage {
        post: Box<dyn Post>,
    },
    // just open the page, don't set new content
    GotoImage,
    OpenWikiPage(String),
    OpenArtistPage(String),
    OpenWikiOrArtist(String, bool),
    GotWiki(WikiEntry),
    // just open the page, don't set new content
    GotoWiki,
    // OpenTags {post:Post},
    Autocomplete {
        search: String,
    },
    Search {
        search: String,
    },
    AddToSearch {
        tag: String,
    },
    ReplaceSearch {
        tag: String,
    },
    LoadNextPage,
    GotIndex {
        page: u32,
        posts: Vec<Box<dyn Post>>,
        skip_viewed: bool,
        is_local: bool,
    },
    GotThumb {
        texture: Option<gtk::gdk::Texture>,
        post: Box<dyn Post>,
    },
    GotImage {
        bytes: bytes::Bytes,
        post: Box<dyn Post>,
    },
    // GotNextPage {index:IndexResult},
    ReloadIndex,
    AddBooru {
        booru: Box<dyn Booru>,
    },
    SetActiveBooru {
        booru: Box<dyn Booru>,
    },
    RemoveBooru {
        booru: String,
    },
    OpenSettings,
    CancelLoading,
    DownloadImage {
        post: Box<dyn Post>,
    },
    DownloadDone {
        post: Box<dyn Post>,
        bytes: bytes::Bytes,
    },
    #[allow(clippy::enum_variant_names)]
    BrowseAction {
        action: BrowseAction,
    },
    SaveSearch,
    RemoveSavedSearch {
        search: String,
    },
    OpenSavedSearch {
        search: String,
        booru: String,
    },
    SetSavedSearchBooru {
        search: String,
        booru: String,
    },
    SetSavedSearchAutoLoad {
        search: String,
        auto: bool,
    },
    Quit,
    // RefreshSavedSearch { search: String },
    // RefreshAllSavedSearches,
    LoadSavedSearchesUntilViewed {
        only_auto_load: bool,
    },
    MakeToast(String),
}

// #[derive(Debug)]
// struct Item {
//     post: dyn Post,
//     thumb: gtk::Image,
// }
// #[derive(Debug)]
// struct Page {
//     items: Vec<Item>,
//     index: u64,
// }
pub const MIN_THUMB_SIZE: i32 = 10;
pub const MAX_THUMB_SIZE: i32 = 1000;

// #[derive(Clone,Debug)]
// enum PbooruPage {
//     SearchPage {search:String},
//     ImagePage {post:Post},
// }

#[derive(PartialEq, Debug)]
pub enum Loading {
    SavedSearch(u32, bool),
    IndexPage,
    Post,
    Done,
}

#[derive(Debug)]
pub struct State {
    pub app: crate::ExampleApplication,
    pub stack: gtk::Stack,
    pub sender: Sender<Action>,
    pub search: String,
    pub page: u32,
    pub last_page_size: u32,

    pub boorus: Vec<BooruData>,
    pub active_booru: Option<String>,
    pub saved_searches: Vec<SavedSearch>,

    pub image_page: Option<ImagePage>,
    pub settings_page: Option<SettingsPage>,

    pub loading: Loading,
    pub last_load: Option<std::time::Instant>,
    pub tagdb: TagDB,

    pub preferences: Preferences,
}

impl State {
    pub fn active_booru_data(&self) -> Option<BooruData> {
        if let Some(active_booru_str) = &self.active_booru {
            for b in &self.boorus {
                if b.booru.get_domain() == active_booru_str {
                    return Some(b.clone_data());
                }
            }
        }
        None
    }

    pub fn active_booru_settings(&self) -> Option<&BooruSettings> {
        if let Some(active_booru_str) = &self.active_booru {
            for b in &self.boorus {
                if b.booru.get_domain() == active_booru_str {
                    return Some(&b.settings);
                }
            }
        }
        None
    }

    pub fn active_booru_settings_mut(&mut self) -> Option<&mut BooruSettings> {
        if let Some(active_booru_str) = &self.active_booru {
            for b in &mut self.boorus {
                if b.booru.get_domain() == active_booru_str {
                    return Some(&mut b.settings);
                }
            }
        }
        None
    }

    // returns the active booru or just the first available one
    pub fn active_booru_data_any(&self) -> Option<BooruData> {
        self.active_booru_data()
            .or_else(|| self.boorus.iter().map(|b| b.clone_data()).next())
    }

    pub fn booru_data(&self, id: &str) -> Option<BooruData> {
        for b in &self.boorus {
            if b.booru.get_domain() == id {
                return Some(b.clone_data());
            }
        }
        None
    }

    pub fn booru_settings(&self, id: &str) -> Option<&BooruSettings> {
        for b in &self.boorus {
            if b.booru.get_domain() == id {
                return Some(&b.settings);
            }
        }
        None
    }

    pub fn booru_settings_mut(&mut self, id: &str) -> Option<&mut BooruSettings> {
        for b in &mut self.boorus {
            if b.booru.get_domain() == id {
                return Some(&mut b.settings);
            }
        }
        None
    }

    pub fn viewed(&self, post: &dyn Post) -> bool {
        match self.booru_settings(post.domain()) {
            Some(settings) => settings.viewed.contains(&post.id()),
            None => false,
        }
    }
}

#[macro_export]
macro_rules! send {
    ($sender:expr, $action:expr) => {
        if let Err(err) = $sender.try_send($action) {
            // eprintln!(
            panic!(
                "Failed to send \"{}\" action due to {}",
                stringify!($action),
                err
            );
        }
    };
}

#[macro_export]
macro_rules! send_async {
    ($sender:expr, $action:expr) => {
        if let Err(err) = $sender.send($action).await {
            // eprintln!(
            panic!(
                "Failed to send \"{}\" action due to {}",
                stringify!($action),
                err
            );
        }
    };
}

#[macro_export]
macro_rules! add_action {
    ($state:expr, $name:expr, $action:expr) => {
        let gaction = gtk::gio::SimpleAction::new($name, None);
        let sender = &($state).sender;
        gaction.connect_activate(clone!(
            #[strong]
            sender,
            move |_, _| {
                send!(sender, $action);
            }
        ));
        ($state).app.add_action(&gaction);
    };
}

#[macro_export]
macro_rules! add_action_value {
    ($state:expr, $name:expr, $action:expr, $value:expr) => {
        let gaction = gtk::gio::SimpleAction::new($name, Some($value));
        let sender = &($state).sender;
        gaction.connect_activate(clone!(
            #[strong]
            sender,
            move |_, arg| {
                if let Some(value) = arg {
                    send!(sender, $action(value.get().unwrap()));
                }
            }
        ));
        ($state).app.add_action(&gaction);
    };
}