From bb07d1b37220a479b2a1d783a2a36f22c2142a3c Mon Sep 17 00:00:00 2001 From: firewire Date: Thu, 19 Mar 2026 02:49:07 -0400 Subject: [PATCH] Done?? --- src/app.rs | 302 +++++++++++++++++++++++++++++++++++++++++++-------- src/types.rs | 28 +++-- 2 files changed, 276 insertions(+), 54 deletions(-) diff --git a/src/app.rs b/src/app.rs index 6ff6d85..87a5381 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,6 +1,13 @@ use crate::types; use crate::util::clamp; use eframe::egui; +use egui::ColorImage; +use futures_util::stream::StreamExt; +use human_bytes::human_bytes; +use image::{ColorType, GenericImage, Rgba}; +use std::cmp::min; +use std::io::Write; +use std::path::Path; use tokio::sync::mpsc; pub struct Application { @@ -12,6 +19,7 @@ pub struct Application { show_side_panel: bool, search_ctx: types::SearchContext, + download_ctx: types::DownloadContext, } impl Application { @@ -38,7 +46,8 @@ impl Application { // 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)) + && ui + .input(|i| i.key_pressed(egui::Key::Enter) && !self.search_ctx.is_searching) { let query = self.query.clone(); let url = self.server_url.clone(); @@ -66,51 +75,58 @@ impl Application { "Page {} of {}", self.search_ctx.page, self.search_ctx.total_pages )); - if ui.button(" - ").clicked() { + if ui.button(" - ").clicked() && !self.search_ctx.is_searching { + let skip = self.search_ctx.page == 1; self.search_ctx.page = clamp(self.search_ctx.page - 1, 1, self.search_ctx.total_pages); - 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; - }); - ctx.forget_all_images(); + if !skip { + 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; + }); + ctx.forget_all_images(); + } } - if ui.button("+").clicked() { + if ui.button("+").clicked() && !self.search_ctx.is_searching { + let skip = self.search_ctx.page == self.search_ctx.total_pages; self.search_ctx.page = clamp(self.search_ctx.page + 1, 1, self.search_ctx.total_pages); - 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; - }); + if !skip { + 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; + }); + ctx.forget_all_images(); + } } ui.label(&self.status); @@ -153,6 +169,113 @@ impl Application { }); } + fn render_center_panel(&mut self, ctx: &egui::Context) { + egui::CentralPanel::default().show(ctx, |ui| { + if !self.search_ctx.search_results.is_empty() { + let cols = 5; + let mut rows = self.search_ctx.search_results.len() / cols; + if self.search_ctx.search_results.len() < cols { + rows = 1; + } + + egui::ScrollArea::vertical().show(ui, |ui| { + egui_extras::StripBuilder::new(ui) + .sizes(egui_extras::Size::relative((rows as f32).recip()), rows) + .vertical(|mut strip| { + for r in 0..rows { + strip.cell(|ui| { + egui_extras::StripBuilder::new(ui) + .sizes( + egui_extras::Size::relative((cols as f32).recip()), + cols, + ) + .horizontal(|mut strip| { + for c in 0..cols { + strip.cell(|ui| { + let file = &self.search_ctx.search_results + [r * cols + c]; + if file.preview != "None" { + ui.add( + egui::Image::from_uri( + file.preview.clone(), + ) + .shrink_to_fit(), + ); + } else { + let mut img_data = image::DynamicImage::new( + 1, + 1, + ColorType::Rgb8, + ); + img_data.put_pixel( + 0, + 0, + Rgba([0, 0, 0, 255]), + ); + let img = ColorImage::from_rgb( + [1, 1], + &img_data.as_bytes(), + ); + let handle = ctx.load_texture( + format!("preview_{}", file.name), + img, + egui::TextureOptions::default(), + ); + let texture = egui::load::SizedTexture::new( + handle.id(), + egui::vec2(1.0, 1.0), + ); + ui.add( + egui::Image::new( + egui::ImageSource::Texture(texture), + ) + .shrink_to_fit(), + ); + } + ui.label(format!("Name: {}", file.name)); + ui.label(format!("Extension: {}", file.ext)); + ui.label(format!( + "Size: {}", + human_bytes(file.size as f64) + )); + ui.horizontal(|ui| { + let download_btn = ui.add_enabled( + !self.download_ctx.is_downloading, + egui::Button::new("Download"), + ); + if download_btn.clicked() { + let path = self + .download_path + .clone() + .unwrap_or("".to_string()); + let url = file.url.clone(); + let (tx, rx) = mpsc::channel::< + Result, + >( + 1 + ); + self.download_ctx.download_rx = + Some(rx); + self.download_ctx.is_downloading = true; + self.status = + "Downloading...".to_string(); + tokio::spawn(async move { + let res = download(url, path).await; + let _ = tx.send(res).await; + }); + } + }); + }) + } + }); + }); + } + }); + }); + } + }); + } + fn process_channels(&mut self, ctx: &egui::Context) { if self.search_ctx.search_rx.is_some() { let mut clear_rx = false; @@ -188,6 +311,36 @@ impl Application { self.search_ctx.search_rx = None; } } + + if self.download_ctx.download_rx.is_some() { + let mut clear_rx = false; + if let Some(rx) = self.download_ctx.download_rx.as_mut() { + match rx.try_recv() { + Ok(Ok(final_path)) => { + self.status = format!("Downloaded {}", final_path); + self.download_ctx.is_downloading = false; + clear_rx = true; + } + Ok(Err(e)) => { + self.status = format!("Download failed: {}", e); + self.download_ctx.is_downloading = false; + clear_rx = true; + } + Err(mpsc::error::TryRecvError::Empty) => { + ctx.request_repaint(); + } + Err(mpsc::error::TryRecvError::Disconnected) => { + self.status = "Download task ended unexpectedly".to_string(); + self.download_ctx.is_downloading = false; + clear_rx = true; + } + } + } + + if clear_rx { + self.download_ctx.download_rx = None; + } + } } } @@ -200,6 +353,7 @@ impl Default for Application { status: "Ready!".to_string(), search_ctx: types::SearchContext::default(), show_side_panel: false, + download_ctx: types::DownloadContext::default(), } } } @@ -211,9 +365,7 @@ impl eframe::App for Application { if self.show_side_panel { self.render_side_panel(ctx); } - egui::CentralPanel::default().show(ctx, |ui| { - ui.heading("// TODO: Put shit here"); - }); + self.render_center_panel(ctx); } } @@ -257,3 +409,67 @@ async fn search_files( Ok((results.results, results.metadata)) } + +async fn download(url: String, dir: String) -> Result { + let file_name = Path::new(&url) + .file_name() + .unwrap() + .to_str() + .unwrap() + .to_string(); + let file_path = Path::new(&dir).join(file_name); + + if file_path.exists() { + return Ok(file_path.to_str().unwrap().to_string()); + } + + let mut file = match std::fs::File::create(file_path.clone()) { + Ok(file) => file, + Err(e) => return Err(format!("Failed to create the file: {}", e)), + }; + 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(&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 total_size_res = response + .content_length() + .ok_or("Failed to get the file size"); + let total_size = match total_size_res { + Ok(res_ok) => res_ok, + Err(e) => return Err(format!("Failed to get the file size: {}", e)), + }; + + let mut downloaded: u64 = 0; + let mut stream = response.bytes_stream(); + while let Some(item) = stream.next().await { + let chunk = item.or(Err(format!( + "Error while downloading file: {}", + url.clone() + )))?; + file.write_all(&chunk).or(Err(format!( + "Error while writing to file: {}", + file_path.to_str().unwrap() + )))?; + let new = min(downloaded + (chunk.len() as u64), total_size); + downloaded = new; + } + Ok(file_path.to_str().unwrap().to_string()) +} diff --git a/src/types.rs b/src/types.rs index 0b976a8..a610bfe 100644 --- a/src/types.rs +++ b/src/types.rs @@ -9,20 +9,26 @@ pub struct Root { #[derive(Debug, Clone, Deserialize)] pub struct FileEntry { - pub name: String, - pub ext: String, + pub name: String, + pub ext: String, #[allow(dead_code)] - pub path: String, - pub url: String, - pub size: i64, + pub path: String, + pub url: String, + pub size: i64, pub preview: String, } #[derive(Debug, Clone, Deserialize)] pub struct Metadata { - pub page: usize, - pub total_pages: usize, - pub page_size: usize + pub page: usize, + pub total_pages: usize, + pub page_size: usize, +} + +#[derive(Default)] +pub struct DownloadContext { + pub is_downloading: bool, + pub download_rx: Option>>, } pub struct SearchContext { @@ -30,8 +36,8 @@ pub struct SearchContext { pub search_rx: Option, Metadata), String>>>, pub search_results: Vec, pub page: usize, - pub per_page : usize, - pub total_pages: usize, + pub per_page: usize, + pub total_pages: usize, pub total_results: usize, } @@ -42,7 +48,7 @@ impl Default for SearchContext { search_rx: None, search_results: vec![], page: 1, - per_page: 25, + per_page: 25, total_pages: 0, total_results: 0, }