use crate::types; use eframe::egui; use tokio::sync::mpsc; pub struct Application { download_path: Option, server_url: String, query: String, status: String, show_side_panel: bool, search_ctx: types::SearchContext, } impl Application { pub fn new(cc: &eframe::CreationContext<'_>) -> Self { cc.egui_ctx.set_visuals(egui::Visuals::dark()); egui_extras::install_image_loaders(&cc.egui_ctx); Self::default() } fn render_top(&mut self, ctx: &egui::Context) { egui::TopBottomPanel::top("top_panel").show(ctx, |ui| { ui.horizontal(|ui| { let mut side_panel_btn_text = ">>"; if self.show_side_panel { side_panel_btn_text = "<<"; } if ui.button(side_panel_btn_text).clicked() { self.show_side_panel = !self.show_side_panel; } ui.separator(); // Search button and input ui.label("Search: "); if ui.text_edit_singleline(&mut self.query).lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) { let query = self.query.clone(); let url = self.server_url.clone(); let page = self.search_ctx.page.clone(); let page_size = self.search_ctx.per_page.clone(); let (tx, rx) = mpsc::channel::< Result<(Vec, types::Metadata), String>, >(1); self.search_ctx.search_rx = Some(rx); self.search_ctx.is_searching = true; self.search_ctx.search_results.clear(); self.search_ctx.page = 0; self.search_ctx.total_pages = 0; self.search_ctx.total_results = 0; self.status = "Searching...".to_string(); tokio::spawn(async move { let res = search_files(url, query, page, page_size).await; let _ = tx.send(res).await; }); } ui.separator(); ui.label(&self.status); }); }); } fn render_side_panel(&mut self, ctx: &egui::Context) { egui::SidePanel::left("left_panel").show(ctx, |ui| { ui.horizontal(|ui| { ui.label("API Url: "); ui.text_edit_singleline(&mut self.server_url); }); ui.horizontal(|ui| { ui.label("Save location: "); ui.label(self.download_path.as_deref().unwrap_or("None")); if ui.button("Browse").clicked() { if let Some(path) = rfd::FileDialog::new().pick_folder() { self.download_path = Some(path.display().to_string()); } } }); }); } fn process_channels(&mut self, ctx: &egui::Context) { if self.search_ctx.search_rx.is_some() { let mut clear_rx = false; if let Some(rx) = self.search_ctx.search_rx.as_mut() { match rx.try_recv() { Ok(Ok(results)) => { // Ok recv, ok results self.search_ctx.search_results = results.0.clone(); self.status = "Ready!".to_string(); self.search_ctx.total_pages = results.1.total_pages; self.search_ctx.page = results.1.page; self.search_ctx.per_page = results.1.page_size; self.search_ctx.is_searching = false; clear_rx = true; } Ok(Err(e)) => { // Ok recv, err results self.status = format!("Search failed: {}", e); self.search_ctx.is_searching = false; clear_rx = true; } Err(mpsc::error::TryRecvError::Empty) => { ctx.request_repaint(); } Err(mpsc::error::TryRecvError::Disconnected) => { self.status = "Search thread ended unexpectedly".to_string(); self.search_ctx.is_searching = false; clear_rx = true; } } } if clear_rx { self.search_ctx.search_rx = None; } } } } impl Default for Application { fn default() -> Self { Application { download_path: None, server_url: "https://stuff.catgirls.fish/search".to_string(), query: "".to_string(), status: "Ready!".to_string(), search_ctx: types::SearchContext::default(), show_side_panel: false, } } } impl eframe::App for Application { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { self.process_channels(ctx); self.render_top(ctx); if self.show_side_panel { self.render_side_panel(ctx); } egui::CentralPanel::default().show(ctx, |ui| { ui.heading("Hello world"); }); } } async fn search_files( url: String, query: String, page: usize, page_size: usize, ) -> Result<(Vec, types::Metadata), String> { let full_url = format!("{}?q={}&p={}&s={}", url, query, page, page_size); let client = match reqwest::Client::builder() .user_agent( "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:141.0) Gecko/20100101 Firefox/141.0", ) .danger_accept_invalid_certs(true) .build() { Ok(client) => client, Err(e) => return Err(format!("Failed to create the client: {}", e)), }; let res = client.get(full_url).send().await; let response = match res { Ok(response_ok) => response_ok, Err(e) => return Err(format!("Failed to download the file: {}", e)), }; if response.status() != reqwest::StatusCode::OK { return Err(format!( "Failed to download the file: {}", response.status() )); } let results = match response.json::().await { Ok(r) => r, Err(e) => { return Err(format!("Failed to deserialize results data: {}", e)); } }; Ok((results.results, results.metadata)) }